红茶的个人站点

  • 首页
  • 专栏
  • 开发工具
  • 其它
  • 隐私政策
Awalon
Talk is cheap,show me the code.
  1. 首页
  2. 专栏
  3. Linux之旅
  4. 正文

Linux 之旅 8:初识 BASH

2021年8月15日 1204点热度 0人点赞 0条评论

image-20210815200301861

认识 BASH 这个 Shell

我们作为用户,并不能和Linux kernel直接交互,而是需要通过一个壳程序(shell)与其交互,而Bash就是最流行的一种shell。

硬件、内核与 shell

Linux内核的主要职责是管理和调度电脑的核心硬件,比如磁盘、内存、屏幕、键盘、CPU等,具体实现是需要加载硬件厂商提供的驱动程序,内核掌管这些硬件之后,就可以利用这些硬件完成一些基本工作,比如从硬盘读取数据到内存,然后由CPU进行运算,得出一个需要的结果,然后按需要显示到屏幕。但是如果直接由用户或者应用程序直接操作内核,一来不方便,这意味着开发者或者用户需要学习内核的工作原理等才能进行开发或者使用Linux,显然不现实。二来这样也会带来一些安全隐患,比如最常见的内存泄露。

所以为了方便应用对内核的调用以及用户的使用,就出现了一种在内核之上的壳程序(shell),应用程序和用户都通过shell与内核进行交互,进而调用底层的硬件设备发挥作用。

所以普通用户和开发者并不需要知道内核的工作原理,只要弄懂shell就可以了,而Bash就是目前Linux上最流行的shell程序。

shell、内核、硬件和用户的关系可以用下图表示:

image-20210814153811999

(图片来资鸟哥的私房菜)

为何要学习 shell

相对于图形界面Xwindow,shell具有以下优点:

  • 一致性:目前Linux上的图形界面XWindow有很多不同的产品,而且UI和功能各不相同,但几乎所有的Linux发行版的shell命令都是一致的。

  • 远程管理:可以使用ssh工具远程连接Linux主机并使用shell命令进行操作。虽然很多安装了XWindow的Linux发行版也可以使用VNC view之类的工具进行图形化远程连接,但是这样一来效率不如命令行模式,二来对远程主机的硬件有要求,必须要能安装XWindow,有不少的限制。

  • 高级功能:XWindow毕竟是面向普通用户的产物,许多高级功能并不能支持,比如被黑客入侵,查看入侵迹象及填补漏洞,就很难用XWindow去完成。

shell 的种类

shell的发展已经有很多年了,所以也有多种shell可供选择,要想知道当前系统中安装的shell,可以:

[icexmoon@xyz ~]$ cat /etc/shells
/bin/sh
/bin/bash
/usr/bin/sh
/usr/bin/bash
/bin/tcsh
/bin/csh

其实这其中只有/bin/bash和/bin/tcsh两种shell,其它都是对这两种shell的符号链接,或者是同样的东西:

[icexmoon@xyz ~]$ ls -al /bin/sh
lrwxrwxrwx. 1 root root 4 7月  24 14:33 /bin/sh -> bash
[icexmoon@xyz ~]$ ls -al /bin/csh
lrwxrwxrwx. 1 root root 4 7月  24 14:38 /bin/csh -> tcsh
[icexmoon@xyz ~]$ ls -al /usr/bin/sh
lrwxrwxrwx. 1 root root 4 7月  24 14:33 /usr/bin/sh -> bash
[icexmoon@xyz ~]$ ls -al /usr/bin/bash
-rwxr-xr-x. 1 root root 964536 4月   1 2020 /usr/bin/bash
[icexmoon@xyz ~]$ ls -al /bin/bash
-rwxr-xr-x. 1 root root 964536 4月   1 2020 /bin/bash

其中tcsh是csh的升级版shell,csh全称C Shell,从这个命名就能看出来,这是利用C语言风格开发的一款shell,所以C程序员可能会比较喜欢用。

bash全称Bourne Again Shell,是在sh(Bourne Shell)的基础上开发的一款shell,也是目前Linux上最流行的shell,基本上所有的Linux发行版都默认使用这种shell,所以我们自然也需要从它开始学习。

Bash shell 的功能

历史记录

bash会记录使用过的命令,并可以通过history命令进行查看。

命令与文件补全

在bash的命令行中,如果我们忘记某个文件或者命令的全名,可以按下Tab键进行尝试补全,按两下可以查看相关候选列表。

命令别名

可以通过alias命令设置命令别名,更方便的使用某些命令或者常用命令+参数,比如alias ll="ls -al"。

任务调度

bash可以将某些长时间执行的任务转移到后台执行,不影响当前操作。

shell脚本

可以通过编写shell脚本的方式实现批处理和自动化。

通配符

在命令中可以使用通配符,比如ls -al /usr/bin/X*,甚至某些命令还支持正则表达式。

type

除了可以在bash中调用ls之类的第三方程序,bash本身还提供一些基本命令,如何查看某个命令是第三方程序还是bash的内建命令?

可以使用type。

如果想查看一个命令的类型,可以:

[icexmoon@xyz ~]$ type xfs_admin
xfs_admin 是 /usr/sbin/xfs_admin
[icexmoon@xyz ~]$ type cd
cd 是 shell 内嵌
[icexmoon@xyz ~]$ type ls
ls 是 `ls --color=auto' 的别名

可以看到,这里分为三种情况,如果是第三方程序,就会显示完整路径,如果是shell的内建命令,就会显示shell build-in,如果是命令别名,就会显示is xxx alias。

如果想查看别名命令的更多信息,比如关联的真实命令的路径,可以:

[icexmoon@xyz ~]$ type -a ls
ls 是 `ls --color=auto' 的别名
ls 是 /usr/bin/ls

如果只想查看命令的类型(写脚本的时候可能会用到):

[icexmoon@xyz ~]$ type -t ls
alias
[icexmoon@xyz ~]$ type -t cd
builtin
[icexmoon@xyz ~]$ type -t xfs_admin
file

返回的信息很明确了,这里不再解释。

指令的下达与快速编辑

如果我们写了很长的一行命令,如果因为可读性方面考虑,可以使用\进行分行:

[icexmoon@xyz ~]$ ls -al / | head -n 5; ls -al /tmp | head -n 5;\
> ls -al /root | head -n 5
总用量 20
dr-xr-xr-x.  18 root root  236 8月   8 21:41 .
dr-xr-xr-x.  18 root root  236 8月   8 21:41 ..
lrwxrwxrwx.   1 root root    7 7月  24 14:33 bin -> usr/bin
dr-xr-xr-x.   5 root root 4096 8月  11 22:04 boot
总用量 36
drwxrwxrwt. 29 root     root     4096 8月  14 16:08 .
dr-xr-xr-x. 18 root     root      236 8月   8 21:41 ..
-rw-r--r--.  1 root     root      176 8月   9 20:18 bashrc_rename
drwx------.  2 icexmoon icexmoon    6 8月   9 16:03 .esd-1000
ls: 无法打开目录/root: 权限不够

这点和python中的\作为续行符的用法是相同的,不同的是我们必须在需要换行的地方使用\后立即使用Enter,另起一行并且光标显示在>后才行,不能随意在一行命令中间的某个地方插入\后进行换行。

之前在命令输入错误,需要删除整行命令的时候我都是使用Backspace一直删除到头部,但其实有更方便的快捷键:

  • Ctrl+u:删除光标之前的命令

  • Ctrl+k:删除光标之后的命令

  • Ctrl+a:移动光标到行首

  • Ctrl+e:移动光标到行尾

变量

整个Bash其实可以看作一个“微型编程语言”,支持变量、流程控制,甚至是函数。

环境变量

和Windows类似,Linux也有环境变量,并且同样可以分为系统环境变量和用户环境变量。系统环境变量对所有用户都生效,而用户环境变量仅对当前登录用户生效。

一般环境变量都使用大写方式定义,比如:

[icexmoon@xyz ~]$ echo $PATH
/usr/local/bin:/usr/bin:/usr/local/sbin:/usr/sbin:/home/icexmoon/.local/bin:/home/icexmoon/bin
[icexmoon@xyz ~]$ echo $SHELL
/bin/bash
[icexmoon@xyz ~]$ echo $HOME
/home/icexmoon

变量设置

打印变量

使用echo命令可以打印变量的值,这点和PHP是很像的,或者说PHP就是借鉴了Bash?

[icexmoon@xyz ~]$ echo $HOME
/home/icexmoon

需要注意的是Bash中操作变量的时候必须使用$符号,而赋值的时候不需要。

初始化和赋值

Bash中变量的初始化很简单,只要使用赋值符号=即可:

[icexmoon@xyz ~]$ message="Hellow Wolrd!"
-bash: !": event not found
[icexmoon@xyz ~]$ message="Hellow Wolrd\!"
[icexmoon@xyz ~]$ echo $message
Hellow Wolrd\!
[icexmoon@xyz ~]$ message='Hellow Wolrd!'
[icexmoon@xyz ~]$ echo $message
Hellow Wolrd!
[icexmoon@xyz ~]$ message=hellow
[icexmoon@xyz ~]$ echo $message
hellow

需要注意的是:

  • Bash中变量同样具有类型,且默认为String。

  • =两边不能有空格。

  • 字符串符号"或'的用法类似于C中的用法,前者包裹的字符串中可以使用特殊字符,除非使用转义符\,后者则会将所有的特殊字符认定为普通字符,所以不需要使用转义符。所以示例中第一个变量声明会出错,因为!是一个特殊字符,但比较奇怪的是使用转义符号后输出中居然同样会有转义符。如果使用单引号则不会出现任何问题。

  • 对于简单的连续性非空字符,可以直接赋值,无需使用引号。

  • 其它注意事项类似于其它常见的编程语言,比如变量命名不能出现特殊字符,以及不能以数字开头等。

字符串连接

似乎Bash中并没有专门的字符串连接符:

[icexmoon@xyz ~]$ person1="Li Lei"
[icexmoon@xyz ~]$ person2="Han Meimei"
[icexmoon@xyz ~]$ all_person=$person1+$person2
[icexmoon@xyz ~]$ echo $all_person
Li Lei+Han Meimei

可以看到结果很粗暴,就是所有内容强行拼到一起,我们自然可以利用这种特性来进行字符串连接:

[icexmoon@xyz ~]$ all_person="$person1 $person2"
[icexmoon@xyz ~]$ echo $all_person
Li Lei Han Meimei

虽然在字符串中依然可以直接使用变量,但某些情况下可能会产生歧义,比如:

[icexmoon@xyz ~]$ person="Zhang San"
[icexmoon@xyz ~]$ all_person="$person1 $person2"
[icexmoon@xyz ~]$ echo $all_person
Li Lei Han Meimei
[icexmoon@xyz ~]$ all_person="${person}1 ${person}2"
[icexmoon@xyz ~]$ echo $all_person
Zhang San1 Zhang San2

看到没,bash并不能区分你是想用$person1这个变量还是$person这个变量,从上边的结果看bash是选择积极匹配,而非惰性匹配,这往往会产生一些歧义,所以建议在类似的情况下使用${person}这样的方式明确指定变量名。

事实上也是因为这个原因,几乎没有哪个主流编程语言是允许直接在字符串中嵌入变量的,PHP倒是可以,不过强制要求使用{}包裹变量,有点像上面的第二种做法。

最常见的字符串连接的用法是修改环境变量PATH:

[icexmoon@xyz ~]$ echo $PATH
/usr/local/bin:/usr/bin:/usr/local/sbin:/usr/sbin:/home/icexmoon/.local/bin:/home/icexmoon/bin
[icexmoon@xyz ~]$ PATH="${PATH}:/tmp"
[icexmoon@xyz ~]$ echo $PATH
/usr/local/bin:/usr/bin:/usr/local/sbin:/usr/sbin:/home/icexmoon/.local/bin:/home/icexmoon/bin:/tmp

执行子语句

在一行命令中,如果我们需要使用某个命令的执行结果,可以使用$(command)这样的写法:

[icexmoon@xyz ~]$ ls -ald /lib/modules/$(uname -r)/kernel
drwxr-xr-x. 12 root root 128 7月  24 14:35 /lib/modules/3.10.0-1160.el7.x86_64/kernel

Linux的内核源代码是放在/lib/modules下边的,并且以内核版本为目录名,单独存放在该子目录下,如果我们要进入该目录或者查询该目录信息就很麻烦了,需要先查看当前系统的内核版本,然后再执行ls或cd命令,但更简单的做法是直接在相应命令中使用$(uname -r),这样就会在执行命令的时候先执行$()包裹的语句,得出一个值,然后将这个值嵌入命令中,再执行整个命令。

除了$()可以包裹子语句,还可以使用`(ESC下方哪个按键):

[icexmoon@xyz ~]$ ls -ald /lib/modules/`uname -r`/kernel
drwxr-xr-x. 12 root root 128 7月  24 14:35 /lib/modules/3.10.0-1160.el7.x86_64/kernel

删除变量

可以使用unset删除变量:

[icexmoon@xyz ~]$ echo $person
Zhang San
[icexmoon@xyz ~]$ unset person
[icexmoon@xyz ~]$ echo $person

我好像有点摸清楚啥时候需要用$啥时候不需要了,似乎需要使用变量的值的时候就使用$,如果直接操作变量本身,比如赋值或者删除,就不需要。

全局变量和局部变量

类似于其它编程语言,Bash中同样有全局变量和局部变量的区别,全局变量可以看作是任何Bash环境下都会预先加载的变量,比如PATH、HOME等,局部变量就是普通情况下用户创建的变量,其作用域仅限于当前的Bash环境,如果进入某个第三方程序,或者打开一个新的Bash,就不能访问到该变量了:

[icexmoon@xyz ~]$ sh
sh-4.2$ echo $person1

sh-4.2$ exit
exit
[icexmoon@xyz ~]$ echo $person1
Li Lei
[icexmoon@xyz ~]$

可以看到通过sh打开一个新的Bash程序,在那个环境中访问$person1,是访问不到的,退出该环境,回到原来的环境再访问,是存在的。

类似于其它编程语言的做法,我们可以将局部变量转变为全局变量,然后其它Bash环境自然也能访问到该变量:

[icexmoon@xyz ~]$ export person1
[icexmoon@xyz ~]$ sh
sh-4.2$ echo $person1
Li Lei
sh-4.2$ exit
exit
[icexmoon@xyz ~]$

如果export后不跟变量名,则会打印当前所有的全局变量:

[icexmoon@xyz /tmp 18:26 #133]$export | tail -n 10
declare -x SHLVL="1"
declare -x SSH_CLIENT="192.168.1.11 7002 22"
declare -x SSH_CONNECTION="192.168.1.11 7002 192.168.1.105 22"
declare -x SSH_TTY="/dev/pts/0"
declare -x TERM="xterm-256color"
declare -x USER="icexmoon"
declare -x XDG_DATA_DIRS="/home/icexmoon/.local/share/flatpak/exports/share:/var/lib/flatpak/exports/share:/usr/local/share:/usr/share"
declare -x XDG_RUNTIME_DIR="/run/user/1000"
declare -x XDG_SESSION_ID="1"
declare -x person1="Li Lei"

环境变量的功能

env

可以使用env(environment)命令列出全部的环境变量:

[icexmoon@xyz ~]$ env | grep -v LS_COLORS
XDG_SESSION_ID=1
HOSTNAME=xyz.icexmoon.centos
SELINUX_ROLE_REQUESTED=
TERM=xterm-256color
SHELL=/bin/bash
HISTSIZE=1000
SSH_CLIENT=192.168.1.11 7002 22
SELINUX_USE_CURRENT_RANGE=
OLDPWD=/lib/modules/3.10.0-1160.el7.x86_64/kernel
SSH_TTY=/dev/pts/0
USER=icexmoon
MAIL=/var/spool/mail/icexmoon
PATH=/usr/local/bin:/usr/bin:/usr/local/sbin:/usr/sbin:/home/icexmoon/.local/bin:/home/icexmoon/bin:/tmp
PWD=/home/icexmoon
LANG=zh_CN.UTF-8
SELINUX_LEVEL_REQUESTED=
HISTCONTROL=ignoredups
SHLVL=1
HOME=/home/icexmoon
LOGNAME=icexmoon
person1=Li Lei
XDG_DATA_DIRS=/home/icexmoon/.local/share/flatpak/exports/share:/var/lib/flatpak/exports/share:/usr/local/share:/usr/share
SSH_CONNECTION=192.168.1.11 7002 192.168.1.105 22
LESSOPEN=||/usr/bin/lesspipe.sh %s
XDG_RUNTIME_DIR=/run/user/1000
_=/usr/bin/env

其中比较重要的有:

  • HOSTNAME:主机名

  • TERM:终端环境

  • SHELL:当前使用的shell程序

  • HISTSIZE:历史记录中能保存的最大命令数目

  • OLDPWD:上一个工作目录(使用cd -可以切换就是领用的这个)

  • USER:用户名

  • MAIL:邮箱位置

  • PWD:当前工作目录

  • LANG:语言和编码

  • HOME:家目录

  • _:用户使用的上一个命令

有一个特殊的环境变量RANDOM,这是Bash的随机数产生器:

[icexmoon@xyz ~]$ echo $RANDOM
3119
[icexmoon@xyz ~]$ echo $RANDOM
3324
[icexmoon@xyz ~]$ echo $RANDOM
8459

其产生的随机数范围为0~32767,且只会产生这个范围内的整数,如果你需要其它的随机数,就要进行转化,比如,如果想产生0~9的随机数:

[icexmoon@xyz ~]$ declare -i random_num=$RANDOM*10/32767; echo $random_num;
0
[icexmoon@xyz ~]$ declare -i random_num=$RANDOM*10/32767; echo $random_num;
8
[icexmoon@xyz ~]$ declare -i random_num=$RANDOM*10/32767; echo $random_num;
0
[icexmoon@xyz ~]$ declare -i random_num=$RANDOM*10/32767; echo $random_num;
3

需要说明的是环境变量同时也是全局变量,所以可以在所有Bash环境中访问。

set

使用set可以查看当前命名空间中的所有变量,包括全局变量和局部变量:

[i cexmoon@xyz ~]$ set | grep HOME
HOME=/home/icexmoon
PROMPT_COMMAND='printf "\033]0;%s@%s:%s\007" "${USER}" "${HOSTNAME%%.*}" "${PWD/#$HOME/~}"'
[icexmoon@xyz ~]$ set | grep "person*"
all_person='Zhang San1 Zhang San2'
person1='Li Lei'
person2='Han Meimei'
[icexmoon@xyz ~]$

其中有几个比较特殊的:

[icexmoon@xyz ~]$ set | grep PS1
PS1='[\u@\h \W]\$ '
[icexmoon@xyz ~]$ set | grep PS2
PS2='> '
  • PS1:提示字符

    PS1就是Bash中光标前的那段描述文字,可以叫做光标提示符。这个内容是可以自定义的,可以使用的特殊符号包括:

    • \d:【星期 月 日】

    • \H:完整主机名

    • \h:主机名中第一个小节的名称

    • \t:时间,24小时【HH:MM:SS】

    • \T:时间,12小时

    • \A,时间,24小时【HH:MM】

    • \@:时间,12小时

    • \u:当前用户名

    • \v:bash版本信息

    • \w:完整工作目录

    • \W:工作目录的目录名

    • \#:命令编号

    • \$:提示字符(root时显示#,其它用户显示$)

    我们可以通过修改该变量来修改光标提示符:

    [icexmoon@xyz ~]$ PS1='[\u@\h \w \A #\#]\$'
    [icexmoon@xyz ~ 18:20 #126]$
    [icexmoon@xyz ~ 18:20 #126]$cd /tmp
    [icexmoon@xyz /tmp 18:20 #127]$
  • $:当前Bash的进程号

    [icexmoon@xyz /tmp 18:20 #127]$echo $$
    1939
  • ?:上一个shell命令执行结果,如果成功,返回0,如果失败,返回非0,一般是错误代码

    [icexmoon@xyz /tmp 18:22 #128]$ls /abc
    ls: 无法访问/abc: 没有那个文件或目录
    [icexmoon@xyz /tmp 18:24 #129]$echo $?
    2
    [icexmoon@xyz /tmp 18:24 #130]$ls -ald /tmp
    drwxrwxrwt. 29 root root 4096 8月  14 16:08 /tmp
    [icexmoon@xyz /tmp 18:24 #131]$echo $?
    0

影响显示结果的语言变量

使用locale可以查看当前系统支持的语言和编码:

[icexmoon@xyz /tmp 18:29 #136]$locale -a | grep zh_CN*
zh_CN
zh_CN.gb18030
zh_CN.gb2312
zh_CN.gbk
zh_CN.utf8
[icexmoon@xyz /tmp 18:29 #137]$

不使用任何参数时,将打印目前系统中语言和编码的相关变量设置:

[icexmoon@xyz /tmp 18:29 #137]$locale
LANG=zh_CN.UTF-8
LC_CTYPE="zh_CN.UTF-8"
LC_NUMERIC="zh_CN.UTF-8"
LC_TIME="zh_CN.UTF-8"
LC_COLLATE="zh_CN.UTF-8"
LC_MONETARY="zh_CN.UTF-8"
LC_MESSAGES="zh_CN.UTF-8"
LC_PAPER="zh_CN.UTF-8"
LC_NAME="zh_CN.UTF-8"
LC_ADDRESS="zh_CN.UTF-8"
LC_TELEPHONE="zh_CN.UTF-8"
LC_MEASUREMENT="zh_CN.UTF-8"
LC_IDENTIFICATION="zh_CN.UTF-8"
LC_ALL=

那个LC_ALL如果设置了,其它LC_*方式命名的变量都会被修改为同样的值,所以如果需要修改语言和编码,只要修改LANG和LC_ALL两个变量即可:

[icexmoon@xyz /tmp 18:30 #138]$LANG=en_US.utf8; LC_ALL=en_US.utf8
[icexmoon@xyz /tmp 18:34 #139]$type cd
cd is a shell builtin
[icexmoon@xyz /tmp 18:34 #140]$LANG=zh_CN.utf8; LC_ALL=zh_CN.utf8
[icexmoon@xyz /tmp 18:35 #141]$type cd
cd 是 shell 内嵌
[icexmoon@xyz /tmp 18:35 #142]$

当然,上边修改当前全局变量的方式仅影响当前环境,重启后就不复存在了,如果需要修改系统启动时默认的语言和编码,需要修改/etc/locale.conf配置文件:

[icexmoon@xyz /tmp 18:35 #142]$cat /etc/locale.conf
LANG="zh_CN.UTF-8"
[icexmoon@xyz /tmp 18:37 #143]$

read,array,declare

read

read用于从键盘读取输入字符串,相当于其它编程语言中的input():

[icexmoon@xyz /tmp 18:37 #143]$read person
朱元璋
[icexmoon@xyz /tmp 18:42 #144]$echo $person
朱元璋
[icexmoon@xyz /tmp 18:42 #145]$read -p "请输入你的姓名:" -t 30 person
请输入你的姓名:嬴政
[icexmoon@xyz /tmp 18:43 #146]$echo $person
嬴政

参数-p可以指定一个字符串作为输入时候显示的提示信息,-t可以设定一个有限的输入等待时间(单位:秒),如果没有在有限定时间内完成输入,将结束等待。

declare,typeset

declare用于声明变量类型:

[icexmoon@xyz /tmp 18:43 #147]$number_test=1+2+3
[icexmoon@xyz /tmp 18:47 #148]$echo $number_test
1+2+3
[icexmoon@xyz /tmp 18:47 #149]$declare -i number
[icexmoon@xyz /tmp 18:47 #150]$echo $number_test
1+2+3
[icexmoon@xyz /tmp 18:47 #151]$number_test=1+2+3
[icexmoon@xyz /tmp 18:47 #152]$echo $number_test
1+2+3
[icexmoon@xyz /tmp 18:47 #153]$unset number_test
[icexmoon@xyz /tmp 18:48 #154]$declare -i number_test
[icexmoon@xyz /tmp 18:48 #155]$number_test=1+2+3
[icexmoon@xyz /tmp 18:48 #156]$echo $number_test
6

可以看到,declare必须在变量创建的时候使用,并指定一个类型,如果变量已经存在,则不会起作用,也就是说变量创建后就无法修改为其它类型的变量,除非使用unset删除变量后重新创建。

declare可以使用以下参数指定变量类型:

  • -a:数组

  • -i:整型

  • -x:全局变量

  • -r:只读变量

需要说明的是Bash中没有浮点数,所以无法保留小数运算结果。

[icexmoon@xyz /tmp 18:55 #159]$export number_test
[icexmoon@xyz /tmp 18:55 #160]$export | grep number_test
declare -ix number_test="6"

将number_test设置为全局变量后使用export查看,可以看到其类型是-ix,也就是整形且是全局变量,可以让其变为只读:

[icexmoon@xyz /tmp 18:55 #161]$declare -r number_test
[icexmoon@xyz /tmp 18:57 #162]$export | grep number_test
declare -irx number_test="6"
[icexmoon@xyz /tmp 18:57 #163]$number_test=7
-bash: number_test: 只读变量
[icexmoon@xyz /tmp 18:58 #164]$unset number_test
-bash: unset: number_test: 无法反设定: 只读 variable

设置为只读后不能修改也不能删除,一般只有注销用户后重新登录才能消失。

如果需要将某个全局变量转化为局部变量,可以:

[icexmoon@xyz /tmp 19:03 #172]$declare +x number_test
[icexmoon@xyz /tmp 19:05 #173]$declare -p number_test
declare -ir number_test="6"
[icexmoon@xyz /tmp 19:05 #174]$

其中decalre -p可以打印指定变量的类型。

对于数组,可以通过以下方式创建:

[icexmoon@xyz /tmp 19:01 #168]$numbers[0]=1
[icexmoon@xyz /tmp 19:02 #169]$numbers[1]=2
[icexmoon@xyz /tmp 19:03 #170]$numbers[2]=3
[icexmoon@xyz /tmp 19:03 #171]$echo "${numbers[0]},${numbers[1]},${numbers[2]}"
1,2,3

typeset与declare的用法类似。

ulimit

我们可以对每个Bash进程设置一些限制,以防止无限制的使用系统资源造成系统资源耗尽。

如果想查看当前的Bash限制信息:

[icexmoon@xyz /tmp 19:06 #175]$ulimit -a
core file size          (blocks, -c) 0
data seg size           (kbytes, -d) unlimited
scheduling priority             (-e) 0
file size               (blocks, -f) unlimited
pending signals                 (-i) 3755
max locked memory       (kbytes, -l) 64
max memory size         (kbytes, -m) unlimited
open files                      (-n) 1024
pipe size            (512 bytes, -p) 8
POSIX message queues     (bytes, -q) 819200
real-time priority              (-r) 0
stack size              (kbytes, -s) 8192
cpu time               (seconds, -t) unlimited
max user processes              (-u) 3755
virtual memory          (kbytes, -v) unlimited
file locks                      (-x) unlimited

其中-f为能创建的单个最大文件大小(单位kB),可以看到现在是unlimited,没有受到限制。

我们修改为10M(10240kB):

[icexmoon@xyz /tmp 19:08 #176]$ulimit -f 10240
[icexmoon@xyz /tmp 19:12 #178]$ulimit -a | grep "file size"
core file size          (blocks, -c) 0
file size               (blocks, -f) 10240
[icexmoon@xyz /tmp 19:12 #179]$dd if=/dev/zero of=ulimit_test bs=1M count=20
文件大小超出限制(吐核)
[icexmoon@xyz /tmp 19:13 #180]$rm -f ulimit_test

需要说明的是,像-f这个设置,一旦设置了,再次修改的时候所设置的值就不能大于当前值了,也就是只能越改越小,无法改大,幸运的是这种设置会在注销用户重新登录后失效。

如果你像我一样是使用ssh登录,只要关掉当前窗口重新打开一个ssh连接就行了。

变量内容的删除、取代与替换

使用#或##符号可以从开始匹配变量的值,并删除符合的内容,使用%或%%可以从尾部开始匹配变量的值,如果匹配到将会被删除:

[icexmoon@xyz ~]$ echo $PATH
/usr/local/bin:/usr/bin:/usr/local/sbin:/usr/sbin:/home/icexmoon/.local/bin:/home/icexmoon/bin
[icexmoon@xyz ~]$ echo ${PATH#/*:}
/usr/bin:/usr/local/sbin:/usr/sbin:/home/icexmoon/.local/bin:/home/icexmoon/bin
[icexmoon@xyz ~]$ echo ${PATH##/*:}
/home/icexmoon/bin
[icexmoon@xyz ~]$ echo ${PATH%:*}
/usr/local/bin:/usr/bin:/usr/local/sbin:/usr/sbin:/home/icexmoon/.local/bin
[icexmoon@xyz ~]$ echo ${PATH%%:*}
/usr/local/bin
[icexmoon@xyz ~]$

这几个符号的区别在于:

  • #:从开头开始,惰性匹配

  • ##:从开头开始,积极匹配

  • %:从结尾开始,惰性匹配

  • %%:从结尾开始,积极匹配

记住上边规律的诀窍在于#都是从开头匹配,%都是从结尾匹配,且符号多的就是积极匹配,符号只有一个就是惰性匹配。

在编程的时候我们有时候会遇到这种情况:如果变量不存在,就赋一个值,如果存在就不处理。Bash也可以处理类似的需求:

[icexmoon@xyz ~]$ unset person
[icexmoon@xyz ~]$ echo ${person-"Li lei"}
Li lei
[icexmoon@xyz ~]$ person="Han Meimei"
[icexmoon@xyz ~]$ echo ${person-"Li lei"}
Han Meimei
[icexmoon@xyz ~]$ person=""
[icexmoon@xyz ~]$ echo ${person-"Li lei"}

[icexmoon@xyz ~]$

这个逻辑很符合我们常用的”如果变量没有设置,就应当是XXX“,当然空字符串也算”变量被设置了“的情况。

如果需要在变量为空字符串的时候同样被设置为我们需要的值,可以:

[icexmoon@xyz ~]$ person=""
[icexmoon@xyz ~]$ echo ${person:-"Li lei"}
Li lei
[icexmoon@xyz ~]$

需要说明的是,上边的示例并不会真的改变变量person的值,如果目的是"变量person如果没有被定义,就给一个初始值"这样的话,应该这样写:

[icexmoon@xyz ~]$ unset person
[icexmoon@xyz ~]$ person=${person-"Li lei"}
[icexmoon@xyz ~]$ echo $person
Li lei
[icexmoon@xyz ~]$ unset person
[icexmoon@xyz ~]$ person="Han Meimei"
[icexmoon@xyz ~]$ person=${person-"Li lei"}
[icexmoon@xyz ~]$ echo $person
Han Meimei
[icexmoon@xyz ~]$

命令别名与History

alias,unalias

有时候,我们需要频繁执行某些复杂的命令,比如查看一个子项目很长的目录:

[icexmoon@xyz ~]$ ls -al /usr/bin | less

我们可以使用alias创建一个便捷的命令别名:

[icexmoon@xyz ~]$ ls -al /usr/bin | less
[icexmoon@xyz ~]$ type lsless
-bash: type: lsless: 未找到
[icexmoon@xyz ~]$ cd /usr/bin
[icexmoon@xyz bin]$ alias lsless="ls -al | less"
[icexmoon@xyz bin]$ lsless

需要注意的是:

  • 可以先使用type command_alias查看你定义的别名名称是否已经被其它命令或别名使用

  • 命令别名有使用限制,比如上边的ls -al /usr/bin | less,我们并不能在别名的指定位置传递一个/usr/bin这样的参数,所以别名也只能设定成ls -al | less这样的内容,使用的时候自然也只能先切换到具体目录后再使用别名,不过似乎可以用将别名定义定义为函数的方式绕过这个限制,详情可以阅读linux中给 alias 添加命令行参数,我将在学习了shell的函数部分后再做此类尝试。

类似的,我们都知道删除命令rm很危险,尤其是这个命令还能将整个目录删除掉,如果意外操作删除了比较重要的文件或目录那怎么办,所以我们可以这样:

[icexmoon@xyz bin]$ type rm
rm 是 /usr/bin/rm
[icexmoon@xyz bin]$ alias rm="rm -i"
[icexmoon@xyz bin]$ type rm
rm 是 `rm -i' 的别名
[icexmoon@xyz bin]$ cd /tmp
[icexmoon@xyz tmp]$ touch rm_test
[icexmoon@xyz tmp]$ rm rm_test
rm:是否删除普通空文件 "rm_test"?y

事实上,为了安全考虑,CentOS中root用户默认就会设置一个这样的命令别名。

如果想查看当前系统中有哪些命令别名,可以:

[icexmoon@xyz tmp]$ alias
alias egrep='egrep --color=auto'
alias fgrep='fgrep --color=auto'
alias grep='grep --color=auto'
alias l.='ls -d .* --color=auto'
alias ll='ls -l --color=auto'
alias ls='ls --color=auto'
alias lsless='ls -al | less'
alias rm='rm -i'
alias vi='vim'
alias which='alias | /usr/bin/which --tty-only --read-alias --show-dot --show-tilde'

如果不想再使用某个命令别名,可以:

[icexmoon@xyz tmp]$ type lsless
lsless 是 `ls -al | less' 的别名
[icexmoon@xyz tmp]$ unalias lsless
[icexmoon@xyz tmp]$ type lsless
-bash: type: lsless: 未找到

history

history可以查看Bash中当前用户执行过的命令的历史记录:

[icexmoon@xyz tmp]$ history
    1  ls
    2  ls -al
    3  exit
    4  ls -al
    5  exit
    6  ls -al
    7  su -
    8  pwd
    9  date
   10  date +%Y-%m-%d
   11  date +%H:%M
   12  cal
   13  cal 01 2021
   ...省略...
  875  type rm
  876  cd /tmp
  877  touch rm_test
  878  rm rm_test
  879  alias
  880  type lsless
  881  unalias lsless
  882  type lsless
  883  history

如果只想查看最近的n条记录,可以:

[icexmoon@xyz tmp]$ history 10
  875  type rm
  876  cd /tmp
  877  touch rm_test
  878  rm rm_test
  879  alias
  880  type lsless
  881  unalias lsless
  882  type lsless
  883  history
  884  history 10

history不仅会将当次登录的命令记录在内存中以供查询,还会在用户退出登录或者关机的时候将历史记录保存在文件中,以在下次登录的时候加载到内存中,便于查询。

记录文件

这个记录文件是~/.bash_history:

[icexmoon@xyz tmp]$ ls -ald ~/.bash_history
-rw-------. 1 icexmoon icexmoon 16185 8月  14 19:48 /home/icexmoon/.bash_history
[icexmoon@xyz tmp]$ cat -n ~/.bash_history | less
[icexmoon@xyz tmp]$ cp ~/.bash_history ~/.bash_history.backup
[icexmoon@xyz tmp]$ history -w
[icexmoon@xyz tmp]$ vim ~/.bash_history

这里我们先查看该文件内容,并保留一个备份.bash_history.backup,然后通过history -w命令将当前内存中的新的历史记录立即写入该文件,并使用vim的多窗口模式进行查看两个文件的差异性:

可以看到,开始内容是一致的:

image-20210815113648196

查看结尾可以看到,写入了今天新执行的命令内容:

image-20210815113914309

比较奇怪的是从行编号可以看到,新的.bash_history文件相比原来的内容,莫名其妙的少了一行,原因不得而知。

除了-w参数,和bash_history文件相关的history参数还有:

  • -r:从bash_history中读取记录到内存

  • -w:将当前内存中的记录写入bash_history,这个操作会覆盖bash_history中的原有记录

  • -a:将内存中的记录追加到bash_history尾部

  • -c:将内存中的记录清空

最后要说明的是bash_history中保存的命令数是有限的,这取决于环境变量HISTSIZE:

[icexmoon@xyz tmp]$ env | grep HIS*
HISTSIZE=1000
HISTCONTROL=ignoredups

这个环境变量规定了bash_history中保留的记录的上限,如果在写入新纪录的时候记录总数超过了这个上限,就会删除一定数目的旧记录后写入新纪录,以保持不超过上限。

这个上限并非越大越好,因为黑客可以通过查看这个文件来查阅某些敏感操作,比如设置数据库密码之类的,从而知道你的一些敏感数据。

如果你需要清除当前用户在系统中的history相关记录,可以:

[icexmoon@xyz ~]$ history -c
[icexmoon@xyz ~]$ history
    1  history
[icexmoon@xyz ~]$ history -w
[icexmoon@xyz ~]$ cat ~/.bash_history
history
history -w

快捷操作

history除了查看记录以外,还可以利用记录进行一些快捷操作:

  1. 执行指定编号的命令:

    [icexmoon@xyz tmp]$ history 10
      888  cp ~/.bash_history ~/.bash_history.backup
      889  history -w
      890  vim ~/.bash_history
      891  export
      892  env
      893  evn | grep HIS*
      894  env | grep HIS*
      895  history
      896  evn | grep HIS*
      897  history 10
    [icexmoon@xyz tmp]$ !894
    env | grep HIS*
    HISTSIZE=1000
    HISTCONTROL=ignoredups

    使用!n就可以重复执行第n条记录,这个非常好用,也很常用。需要注意的就是不要把编号看错了。

  2. 执行最近的一条以XX开头的命令:

    [icexmoon@xyz tmp]$ !env
    env | grep HIS*
    HISTSIZE=1000
    HISTCONTROL=ignoredups

    使用!xxx就可以执行上一条以xxx开头的命令。

  3. 执行上一条命令:

    [icexmoon@xyz tmp]$ !!
    env | grep HIS*
    HISTSIZE=1000
    HISTCONTROL=ignoredups

    使用!!可以重复执行上一条命令,在这个示例中上一条是!env,所以这里重复执行了!env,进而执行了env | grep HIS*。

同一账号登录

需要注意的是,history是按照账户为单位保留记录的(~/.bash_history),所以自然的,如果你使用多个终端同时使用同一个账号登录Linux主机,会按照终端退出的顺序依次写入操作记录,如果每个终端写入的记录条数都很多,就很可能出现后写入的记录“覆盖”掉先写入的记录的情况,这就可能导致某些最近执行的关键操作记录“丢失”。

对于这种现象,可以通过尽量使用单一终端登录进行操作来避免。

Bash Shell 的操作环境

路径与指令搜寻顺序

当我们在Bash命令行中输入一行命令并按下Enter后,Bash会以以下顺序检索可以调用的命令然后执行:

  • 相对路径、绝对路径指定的可执行文件

  • 命令别名

  • bash的内置命令

  • 从环境变量PATH中包含的目录中检索

可以看到命令别名的优先级还是挺高的,仅次于相对路径或绝对路径,这也就是为什么如果我们要执行vi而非vim的时候需要使用vi的完整路径或者\vi。

之前我们说过type -a可以查看某个指令的所有可调用方式,事实上还可以查看Bash调用某个命令的检索顺序:

[icexmoon@xyz tmp]$ type -a vi
vi 是 `vim' 的别名
vi 是 /usr/bin/vi

就像vi,同时存在命令别名和PATH中的/usr/bin/vi可执行文件,而别名是优先于PATH的。

Bash 的欢迎信息

在Linux主机上打开一个终端的时候,会显示一行欢迎信息:

image-20210815131055464

  • 按下Ctrl+Alt+F2切换到tty2就能看到

  • 使用XWindow的tty1是没有的,使用sh远程登录的时候同样不会有

事实上这行欢迎信息的格式由配置文件/etc/issue所决定:

[icexmoon@xyz tmp]$ cat /etc/issue
\S
Kernel \r on an \m

这个文件中的特殊字符的含义为:

  • \d:本地的时间和日期

  • \t:本地时间

  • \l:第几个终端接口

  • \S:Linux发行版名称

  • \r:Linux内核版本

  • \m:CPU架构,如i386\i486等,目前一般为x86_64

  • \n:主机的网络名称

  • \O:主机名hostname

如果需要个性化的欢迎信息,可以用root修改这个配置文件的内容:

[icexmoon@xyz tmp]$ su -
密码:
上一次登录:日 8月 15 13:16:46 CST 2021pts/0 上
[root@xyz ~]# cp -a /etc/issue /etc/issue.backup
cp:是否覆盖"/etc/issue.backup"? y
[root@xyz ~]# vi /etc/issue
[root@xyz ~]# cat /etc/issue
\S
Kernel \r on an \m
\O \n
Date: \d \t
tty: \l

重启虚拟机后切换到tty2就可以看到:

image-20210815132646800

如果不喜欢,可以使用备份还原配置文件:

[icexmoon@xyz ~]$ sudo cp /etc/issue.backup /etc/issue
[sudo] icexmoon 的密码:
[icexmoon@xyz ~]$ cat /etc/issue
\S
Kernel \r on an \m

此外还有一个/etc/issue.telnet的配置文件,这个文件是为telnet远程登录时候的欢迎信息准备的,不过好像现在一般都是使用ssh进行远程登录,所以应该用处不大了。

如果你作为Linux主机的管理员,想要编写一段信息,给每个登录该主机的用户看,可以修改/etc/motd文件:

[icexmoon@xyz ~]$ ls -al /etc/motd
-rw-r--r--. 1 root root 0 6月   7 2013 /etc/motd
[icexmoon@xyz ~]$ sudo vi /etc/motd
[icexmoon@xyz ~]$ cat /etc/motd
Hellow, everyone.
Hellow Wolrd!
[icexmoon@xyz ~]$

打开新的ssh窗口登录:

icexmoon@192.168.1.105's password:
Last login: Sun Aug 15 13:29:02 2021 from icexmoon-book
Hellow, everyone.
Hellow Wolrd!
[icexmoon@xyz ~]$

要恢复原样只要编辑文件,使用dd删除内容后保存即可。

Bash的环境配置文件

这里有个概念需要介绍:login shell 与non-login shell。

简单地讲,login shell就是需要登录的shell,non-login shell自然是不需要登录的shell。

具体来说,当我们开机后通过tty1的XWindow登录的shell,或者是通过tty2等字符终端登录的shell,亦或者是通过ssh远程连接登录的shell,都属于login shell。

而在已经登录的环境中,比如XWindow中打开一个Bash应用,此时就是一个non-login shell。当然在已经登录的ssh中使用sh命令打开一个子shell也同样是non-login shell。

之所以要将shell区分为login与non-login,原因在于安全方面的考虑。当用户通过login shell登录系统后,shell将会根据当前登录用户的UID加载相关的环境变量,让整个Bash环境都”变成当前用户的操作环境“,比如HOME或MAIL,显然每个用户具有不同的家目录和邮箱路径。

而non-login则不同,它只会简单地开启一个新的shell,至于相关的shell环境变量,都会直接使用当前的login shell加载的那些,也就是说non-login shell并不会改变用户在Bash中的操作环境。

这看起来是理所应当的,毕竟non-login shell不会要求用户验证登录,那如果还能转变成另一个人的Bash环境岂不是乱套了。

那么这一切是怎么实现的?

这是因为login shell与non-login shell加载的环境配置文件有所不同。

login shell的环境配置文件加载流程可以用下图表示:

image-20210815142116448

(图片来自鸟哥的私房菜)

也就是说Bash会依次加载/etc/profile、~/.bash_profile,同时/etc/profile也会加载/etc/profile.d目录中的shell脚本,对应的shell脚本也会加载一些额外的配置文件,比如/etc/locale.conf,而~/.bash_profile也会执行类似的操作加载额外的shell脚本和配置文件。

简单说一下这些配置文件的用途:

  • /etc/profile:

    Bash的主配置文件,在这个文件中,会根据登录用户的UID设置一些重要的环境变量和设置,比如:

    • PATH

    • MAIL

    • USER

    • HOSTNAME

    • HISTSIZE

    • umask(root为022,一般用户为002)

  • /etc/profile.d/*.sh:

    这里代表/etc/profile.d目录下的所有shell脚本,这些脚本会有不同的用途,包括:规定Bash接口的颜色、语言和编码,设定命令别名等。

  • /etc/locale.conf:

    由/etc/profile.d/lang.sh调用,存放语言和编码的相关设置

  • ~/.bash_profile:

    这里一般存放使用者的个人配置,事实上有三个具有同样作用的配置文件:

    • ~/.bash_profile

    • ~/.bash_login

    • ~/.profile

    但事实上Bash仅会按顺序加载其中之一,也就是说如果存在~/.bash_profile,则不会加载其余两个。否则查找~/.bash_login,如果存在就加载,不会加载~/.profile。如果没有,则加载~/.profile。

    这样做是为了兼容其它的Linux发行版。

    [icexmoon@xyz ~]$ cat ~/.bash_profile
    # .bash_profile
    
    # Get the aliases and functions
    if [ -f ~/.bashrc ]; then
            . ~/.bashrc
    fi
    
    # User specific environment and startup programs
    
    PATH=$PATH:$HOME/.local/bin:$HOME/bin
    
    export PATH

    可以看到目前~/.bash_profile做了两件事:

    • 尝试加载~/.bashrc文件

    • 给PATH变量追加一个$HOME/bin的路径,并且将PATH设置为全局变量

    注释User specific environment and startup programs提醒我们,可以在这个文件中追加用户自定义的环境变量和开机自启动程序。

  • ~/.bashrc:

    [icexmoon@xyz ~]$ cat ~/.bashrc
    # .bashrc
    
    # Source global definitions
    if [ -f /etc/bashrc ]; then
            . /etc/bashrc
    fi
    
    # Uncomment the following line if you don't like systemctl's auto-paging feature:
    # export SYSTEMD_PAGER=
    
    # User specific aliases and functions

    这个文件是~/.bash_profile加载的,这个文件初始的唯一作用是尝试加载/etc/bashrc,此外注释User specific aliases and functions提示我们可以在这个文件中编写用户自定义命令别名和函数。

  • /etc/bashrc:

    这个配置文件的主要作用有:

    • 根据UID设置umask

    • 根据UID设置提示字符PS1

    • 调用/etc/profile/*.sh的相关shell脚本

    可能有人会问这些工作不是前边/etc/profile已经执行过了吗,这是因为non-login shell并不会加载/etc/profile,但是会记载~/.bash_profile,进而加载/etc/bashrc。所以像PS1这个变量,并不是全局的,如果这里不再设置一遍,那么non-login shell就不会有这个变量,也不会有好看的命令提示信息。

下面说一下non-login shell的加载过程,其实很简单,non-login shell只会加载~/.bash_profile,以及~/.bash_profile额外加载的一些配置。也就是说它并不会加载/etc/profile相关的配置。

推荐将用户自定义的配置放在~/.bash_profile或者~/.bashrc中,比如:

[icexmoon@xyz ~]$ vim .bash_profile
[icexmoon@xyz ~]$ cat .bash_profile
# .bash_profile

# Get the aliases and functions
if [ -f ~/.bashrc ]; then
        . ~/.bashrc
fi

# User specific environment and startup programs

PATH=$PATH:$HOME/.local/bin:$HOME/bin

export PATH
MY_EMAIL="icexmoon@qq.com"
export MY_EMAIL
[icexmoon@xyz ~]$ source .bash_profile
[icexmoon@xyz ~]$ echo $MY_EMAIL
icexmoon@qq.com
[icexmoon@xyz ~]$

我这里定义了一个自定义全局变量MY_EMAIL,修改好配置文件后,需要使用source命令进行加载后生效,使用.命令也可以,比如. .bash_profile。

关于login shell和non-login shell的更多信息,可以阅读login shell和non-login shell。

除了上边介绍的Bash相关的核心配置文件外,还有以下和Bash有关的配置文件:

  • etc/man_db.conf:

    man命令使用的帮助文档检索路径相关的配置文件,如果你使用非系统软件包方式安装应用,比如直接编译源码,就无法使用man查看相关应用的帮助信息,除非自行在这个配置文件中添加相关帮助文档的路径。

  • ~/.bash_history:

    history保存的命令记录,上边介绍过。

  • ~/.bash_logout:

    用户注销时候会执行的配置文件,如果你需要一些任务在用户注销时执行,比如说调用sync强制同步数据,可以放在这个配置文件中。

stty,set

在Bash的字符终端中可以使用Ctrl+C来终止程序的执行,这个想必大家都知道,此外还有哪些可以使用的快捷键呢:

[icexmoon@xyz ~]$ stty -a
speed 9600 baud; rows 33; columns 133; line = 0;
intr = ^C; quit = ^\; erase = ^?; kill = ^U; eof = ^D; eol = <undef>; eol2 = <undef>; swtch = <undef>; start = ^Q; stop = ^S;
susp = ^Z; rprnt = ^R; werase = ^W; lnext = ^V; flush = ^O; min = 1; time = 0;
-parenb -parodd -cmspar cs8 -hupcl -cstopb cread -clocal -crtscts
-ignbrk -brkint -ignpar -parmrk -inpck -istrip -inlcr -igncr icrnl ixon -ixoff -iuclc -ixany -imaxbel -iutf8
opost -olcuc -ocrnl onlcr -onocr -onlret -ofill -ofdel nl0 cr0 tab0 bs0 vt0 ff0
isig icanon iexten echo echoe echok -echonl -noflsh -xcase -tostop -echoprt echoctl echoke

使用stty -a可以查看Bash中可以使用的快捷键,下面简单说明:

  • Ctrl+c:给正在执行的程序发送一个中断信号,其结果取决于程序本身的行为,大多数程序员都会为让程序在收到该中断后立即结束运行(非正常退出),vim会提醒用户是否想退出。

  • Ctrl+\:给正在执行的程序发送一个quit信号。

  • Ctrl+w:删除光标前的单词

  • Ctrl+?:撤销上一个操作(比如之前用Ctrl+w删除了前一个单词,使用Ctrl+?将恢复该单词)

  • Ctrl+u:删除整行命令

  • Ctrl+d:输入一个eof字符(通常用于从键盘读取输入的时候结束输入)

  • Ctrl+s:停止程序,此时程序会被挂起,比如在使用vim的时候按下Ctrl+s,虽然还能看到字符界面,但是程序不会做出任何响应,就像死机一样。

  • Ctrl+q:让挂起的程序继续执行

  • Ctrl+z:将前台运行的程序转到后台并挂起,可以通过其它方式让其在后台继续执行。

此外,你也可以通过stty命令自定义快捷键:

 stty erase ^h

这样就可以使用Ctrl+h来进行撤销动作,当然最好不要这么做,毕竟默认的热键是每个shell环境都一样的,如果自己改了那就不能在其它shell中愉快的玩耍了。

除了快捷键,shell还有一些其它一般性设置:

[icexmoon@xyz ~]$ echo $-
himBH
[icexmoon@xyz ~]$

变量-保存着的值就是shell的一般性设置,这些设置的含义为:

  • h:默认启用,与history有关

  • u:默认不启用,开启后,如果使用的变量没有定义,会报错

  • m:默认启用,与任务调度有关

  • B:默认启用,与[]有关

  • H:默认启用,与history有关

可以使用set进行设置:

[icexmoon@xyz ~]$ set -u
[icexmoon@xyz ~]$ echo $test_set
-bash: test_set: 为绑定变量
[icexmoon@xyz ~]$ set +u
[icexmoon@xyz ~]$ echo $test_set

[icexmoon@xyz ~]$

set +u可以取消相应设置。

通配符

Bash下的大部分命令都支持通配符,有些甚至支持正则。而通配符本身就可以看作是一个精简版的正则。

通配符主要包含:

  • *:任意多的任意字符

  • ?:一个任意字符

  • []:一定范围内的一个字符,比如[abc]表示要么是a,要么是b,要么是c。更常见的用法是用[0-9]表示一个数字,用[a-zA-Z]表示一个字母,用[a-z]表示一个小写字母。

  • [^]:表示一定范围以外的任意字符,比如[^0-9]表示非数字

实际演示:

[icexmoon@xyz ~]$ LANG=C
[icexmoon@xyz ~]$ ls -ald /etc/[^a-z]*
-rw-r--r--. 1 root root 5090 Aug  6  2019 /etc/DIR_COLORS
-rw-r--r--. 1 root root 5725 Aug  6  2019 /etc/DIR_COLORS.256color
-rw-r--r--. 1 root root 4669 Aug  6  2019 /etc/DIR_COLORS.lightbgcolor
-rw-r--r--. 1 root root   94 Mar 25  2017 /etc/GREP_COLORS
-rw-r--r--. 1 root root 1704 Aug 13  2019 /etc/GeoIP.conf
drwxr-xr-x. 8 root root  145 Jul 24 14:37 /etc/NetworkManager
drwxr-xr-x. 2 root root   92 Jul 24 14:38 /etc/PackageKit
drwxr-xr-x. 2 root root   25 Jul 24 14:36 /etc/UPower
drwxr-xr-x. 6 root root  103 Jul 24 14:35 /etc/X11
[icexmoon@xyz ~]$ ls -ald /etc/*[0-9]* | head -n 10
-rw-r--r--. 1 root root 5725 Aug  6  2019 /etc/DIR_COLORS.256color
drwxr-xr-x. 6 root root  103 Jul 24 14:35 /etc/X11
drwxr-xr-x. 4 root root   78 Jul 24 14:35 /etc/dbus-1
-rw-r--r--. 1 root root  112 Sep 30  2020 /etc/e2fsck.conf
lrwxrwxrwx. 1 root root   22 Jul 24 14:35 /etc/grub2.cfg -> ../boot/grub2/grub.cfg
drwxr-xr-x. 2 root root  159 Jul 24 14:34 /etc/iproute2
-rw-r--r--. 1 root root  646 Sep 30  2020 /etc/krb5.conf
drwxr-xr-x. 2 root root    6 Oct  1  2020 /etc/krb5.conf.d
-rw-r--r--. 1 root root 1106 Sep 30  2020 /etc/mke2fs.conf
-rw-r--r--. 1 root root 1362 Jun 10  2014 /etc/pbm2ppa.conf
[icexmoon@xyz ~]$ ls -ald /etc/cron* | head -n 10
drwxr-xr-x. 2 root root  54 Jul 24 14:38 /etc/cron.d
drwxr-xr-x. 2 root root  57 Jul 24 14:38 /etc/cron.daily
-rw-------. 1 root root   0 Aug  9  2019 /etc/cron.deny
drwxr-xr-x. 2 root root  41 Jul 24 14:38 /etc/cron.hourly
drwxr-xr-x. 2 root root   6 Jun 10  2014 /etc/cron.monthly
drwxr-xr-x. 2 root root   6 Jun 10  2014 /etc/cron.weekly
-rw-r--r--. 1 root root 451 Jun 10  2014 /etc/crontab
[icexmoon@xyz ~]$ LANG=zh_CN.UTF-8

实际使用中发现通配符与当前环境的语言和编码有关,比如在zh_CN.UTF-8下,[^a-z]这种通配符是执行不了的,切换语言为LANG=C之后就可以执行了。

更多通配符的使用示例和用法可以阅读通配符。

数据流重定向

如果懂编程,应该知道stdin和stdout两个东西,前者是标准输入流,一般来说默认就是键盘,后者是标准输出流,一般默认是屏幕。

计算机世界中所有的数据都是以流的方式进行传递的,比如说从内存赋值数据到文件,就需要先在硬盘上打开一个输入用的套接字,然后在内存中打开一个输出用的套接字,然后建立管道,让数据从内存流向硬盘。

讲的不一定对哈,时间久了,具体记不清了,大概是这么个意思。

而stdin可以看作是程序使用中接收数据的管道,也就是输入流,stdout是程序使用中输出数据的管道,也就是输出流。此外还有个stderror,是用来输出错误信息的管道。

一般stdin默认连接键盘,也就是说从键盘输入数据到程序,stdout默认连接屏幕,也就是说程序的正常输出信息会显示到屏幕,stderror也默认连接屏幕,所以错误信息同样会显示在屏幕上。

数据流和现实中的水管或者电线线路是一样的,虽然默认会这么接,但是我们可以通过数据流重定向来改变数据流的连接方向,改变数据流向,比如:

[icexmoon@xyz ~]$ ls -al / | head -n 10
总用量 20
dr-xr-xr-x.  18 root root  236 8月   8 21:41 .
dr-xr-xr-x.  18 root root  236 8月   8 21:41 ..
lrwxrwxrwx.   1 root root    7 7月  24 14:33 bin -> usr/bin
dr-xr-xr-x.   5 root root 4096 8月  11 22:04 boot
drwxr-xr-x.   6 root root   51 8月   9 15:15 data
drwxr-xr-x.  20 root root 3380 8月  15 13:23 dev
drwxr-xr-x. 139 root root 8192 8月  15 13:37 etc
drwxr-xr-x.   3 root root   22 7月  24 14:45 home
lrwxrwxrwx.   1 root root    7 7月  24 14:33 lib -> usr/lib
[icexmoon@xyz ~]$ ls -al / | head -n 10 > /tmp/std_test
[icexmoon@xyz ~]$ cat /tmp/std_test
总用量 20
dr-xr-xr-x.  18 root root  236 8月   8 21:41 .
dr-xr-xr-x.  18 root root  236 8月   8 21:41 ..
lrwxrwxrwx.   1 root root    7 7月  24 14:33 bin -> usr/bin
dr-xr-xr-x.   5 root root 4096 8月  11 22:04 boot
drwxr-xr-x.   6 root root   51 8月   9 15:15 data
drwxr-xr-x.  20 root root 3380 8月  15 13:23 dev
drwxr-xr-x. 139 root root 8192 8月  15 13:37 etc
drwxr-xr-x.   3 root root   22 7月  24 14:45 home
lrwxrwxrwx.   1 root root    7 7月  24 14:33 lib -> usr/lib

默认cat命令的执行结果是直接输出到屏幕的,这是因为它连接着stdout,而stdout默认就是连接的屏幕,我们可以使用>符号讲stdout重定向到文件/tmp/std_test,此时再执行,屏幕上就不会现实任何执行结果了,而相应的会生成一个文件,该文件的内容就是执行结果。

类似的,我们还可以对stdin以及stderror进行重定向:

  • >或>>,对stdout进行重定向

  • 2>或2>>,对stderror进行重定向

  • <或<<,对stdin进行重定向

之所以重定向stderror是2>,是因为这三个数据流是由数字进行区分的,0表示stdin,1表示stdout,2表示stderror。

>与>>的区别在于,前者会覆盖数据流那一头的原有数据,而>>是在原有数据基础上追加:

[icexmoon@xyz ~]$ ls -al /my_test 2> /tmp/std_test
[icexmoon@xyz ~]$ cat /tmp/std_test
ls: 无法访问/my_test: 没有那个文件或目录
[icexmoon@xyz ~]$ ls -al /my_test 2>> /tmp/std_test
[icexmoon@xyz ~]$ cat /tmp/std_test
ls: 无法访问/my_test: 没有那个文件或目录
ls: 无法访问/my_test: 没有那个文件或目录

但对于stdin,<与<<的含义略有不同,我们先看一个cat命令的例子:

[icexmoon@xyz ~]$ cat > /tmp/std_test
Helow World!!!
[icexmoon@xyz ~]$ cat /tmp/std_test
Helow World!!!
[icexmoon@xyz ~]$

这里使用>重定向stdout到文件,此时cat程序会通过stdin读入数据,而stdin默认是键盘,所以自然会出现一个光标等待用户输入,输入文字后使用Ctrl+d输入eof字符就可以结束输入(记得要按Enter添加一个换行符先,否则显示会错位)。

下面使用<重定向stdin:

[icexmoon@xyz ~]$ cat > /tmp/std_test < ~/.bash_profile
[icexmoon@xyz ~]$ cat /tmp/std_test
# .bash_profile

# Get the aliases and functions
if [ -f ~/.bashrc ]; then
        . ~/.bashrc
fi

# User specific environment and startup programs

PATH=$PATH:$HOME/.local/bin:$HOME/bin

export PATH
MY_EMAIL="icexmoon@qq.com"
export MY_EMAIL
[icexmoon@xyz ~]$

我们这里使用<将输入流重定向到文件~/.bash_profile,所以cat会先从stdin读入~/.bash_profile的内容,然后通过stdout输出到/tmp/std_test,最终的效果就是std_test中的内容与~/.bash_profile一摸一样,相当于使用cp命令,是不是很有趣?

上面从键盘输入的那个示例中我们需要按Ctrl+d来结束输入,可不可以在检测到某个字符后自动结束输入呢?

当然是可以的:

[icexmoon@xyz ~]$ rm -f /tmp/std_test
[icexmoon@xyz ~]$ cat > /tmp/std_test << '\n'
> Hellow Wolrd
> \n
[icexmoon@xyz ~]$ cat /tmp/std_test
Hellow Wolrd
[icexmoon@xyz ~]$

这里通过<<指定出现\n字符后关闭输入流,所以在出现光标输入文字的时候,如果我们想结束输入,就输入一个\n,然后就会自动结束输入,不需要再使用Ctrl+d。

此外Linux还有一个特殊的设备/dev/null,这个设备就像黑洞一样,任何导入到这个设备的数据流都会消失,所以对于不需要的数据流,比如我们不想看到某些命令执行中的错误信息,可以这样:

[icexmoon@xyz ~]$ ls /my_test
ls: 无法访问/my_test: 没有那个文件或目录
[icexmoon@xyz ~]$ ls /my_test 2> /dev/null

当然,前提是这些错误信息的确没用,否则最好还是保存到日志文件比较好。

有时候我们想同时记录文件的正常输出和错误输出,就像默认stdout和stderror都指向屏幕一样,我们可以这样:

[icexmoon@xyz ~]$ tar -cvf ~/tmp.tar /tmp 1> tmp.tar.log 2>&1
[icexmoon@xyz ~]$ cat tmp.tar.log | less

这样也可以:

[icexmoon@xyz ~]$ tar -cvf ~/tmp.tar /tmp 2> tmp.tar.log 1>&2
[icexmoon@xyz ~]$ cat tmp.tar.log | less

或者:

[icexmoon@xyz ~]$ tar -cvf ~/tmp.tar /tmp &> tmp.tar.log
[icexmoon@xyz ~]$ cat tmp.tar.log | less

前两个的写法是先将stdin或者stderorr重定向,然后再将另一个输出流重定向到前者,并且使用&字符表示是融合在一起进行输出,最后一个写法简单粗暴,就是用&>将两个输出流都重定向。

逻辑运算符

在shell命令中同样可以使用逻辑运算符&&与||,其用途与C语言中别无二致,也就是说除了可以运算bool结果,还具有”旁路“的附加特性。

我们都知道&&类似于物理中的串联电路,只要其中一个表达式为False,就会停止继续执行其它表达式,直接返回False,而||类似于物理中的并联电路,只要其中一个表达式为True,就会停止继续执行其它表达式,直接返回True(本质上是离散数学中的概念,不过我觉得用电路类比更容易理解)。

出于执行效率考虑,几乎所有的编程语言对于处理这两种逻辑运算符都采用类似的处理逻辑,Bash也不例外,而这种额外特性就可以用来实现一些特殊的目的:

[icexmoon@xyz ~]$ ls -al /tmp/test && cp /tmp/test /tmp/test2
ls: 无法访问/tmp/test: 没有那个文件或目录
[icexmoon@xyz ~]$ touch /tmp/test
[icexmoon@xyz ~]$ ls -al /tmp/test && cp /tmp/test /tmp/test2
-rw-rw-r--. 1 icexmoon icexmoon 0 8月  15 17:24 /tmp/test
[icexmoon@xyz ~]$ ls -al /tmp/test2
-rw-rw-r--. 1 icexmoon icexmoon 0 8月  15 17:24 /tmp/test2

这里利用&&实现了一个只有文件/tmp/test存在,才进行复制的逻辑。当文件不存在的时候,&&后的复制操作就不会执行,因为此时前边一个表达式的执行结果是False,已经”旁路“掉了后一个表达式的执行,所以屏幕上并没有后一个表达式的错误输出。

类似的,我们也可以利用||实现第一个表达式为False才执行第二个表达式这样的逻辑:

[icexmoon@xyz ~]$ ls -al /tmp/test3 || touch /tmp/test3
ls: 无法访问/tmp/test3: 没有那个文件或目录
[icexmoon@xyz ~]$ ls -al /tmp/test3
-rw-rw-r--. 1 icexmoon icexmoon 0 8月  15 17:27 /tmp/test3

这里的是逻辑是只有文件/tmp/test3不存在,才会创建/tmp/test3。

虽然逻辑表达式这种写法很简洁,但是并不提倡这么使用,因为这种写法很难阅读,同时在编程领域,这种逻辑表达式的旁路效果更多的是被认为一种附带特性,并不应该去刻意使用,如果你某个表达式是否执行依赖于前一个表达式的运行结果,应该使用if/else控制流,而非使用逻辑表达式。

管道命令

前面我们已经使用过|这个管道符号了,它的作用可以用下图来表示:

image-20210815173406429

(图片来自鸟哥的私房菜)

也就是说利用|,可以让前一个命令的stdout接到后一个命令的stdin上。

需要说明的是,默认情况下管道符号仅会”转接“前一个命令的stdout,stderror会被丢弃,如果需要同时转接stderror,就需要在前一个命令中使用2>&1来将stderror转接到stdout上,再一起转接到后一个命令的stdin。

cut,grep

cut

cut命令可以以行为单位,指定字符作为分隔符对字符串进行截取,类似于Python中的字符串函数partition:

[icexmoon@xyz ~]$ echo ${PATH}
/usr/local/bin:/usr/bin:/usr/local/sbin:/usr/sbin:/home/icexmoon/.local/bin:/home/icexmoon/bin:/tmp:/home/icexmoon/.local/bin:/home/icexmoon/bin:/home/icexmoon/.local/bin:/home/icexmoon/bin:/home/icexmoon/.local/bin:/home/icexmoon/bin
Try 'cut --help' for more information.
[icexmoon@xyz ~]$ echo ${PATH} | cut -d ':' -f 3,5
/usr/local/sbin:/home/icexmoon/.local/bin
[icexmoon@xyz ~]$ echo ${PATH} | cut -d ':' -f 3
/usr/local/sbin

这个例子是从变量PATH中截取指定的目录,其中-d用于指定分隔符,-f用于指定分割后的段落序号。

[icexmoon@xyz ~]$ last | tail -n 10
icexmoon :0           :0               Thu Jul 29 15:48 - 16:37  (00:48)
reboot   system boot  3.10.0-1160.el7. Thu Jul 29 15:48 - 21:24 (8+05:35)
icexmoon :0           :0               Thu Jul 29 15:43 - down   (00:04)
reboot   system boot  3.10.0-1160.el7. Thu Jul 29 15:42 - 15:48  (00:05)
icexmoon :0           :0               Tue Jul 27 12:50 - crash (2+02:51)
reboot   system boot  3.10.0-1160.el7. Tue Jul 27 12:49 - 15:48 (2+02:58)
icexmoon :0           :0               Sat Jul 24 14:49 - crash (2+22:00)
reboot   system boot  3.10.0-1160.el7. Sat Jul 24 14:47 - 15:48 (5+01:00)

wtmp begins Sat Jul 24 14:47:46 2021
[icexmoon@xyz ~]$ last | tail -n 10 | cut -d ' ' -f 1
icexmoon
reboot
icexmoon
reboot
icexmoon
reboot
icexmoon
reboot

wtmp

这个例子用于显示最近登录系统的用户名。

[icexmoon@xyz ~]$ export | head -n 5
declare -x HISTCONTROL="ignoredups"
declare -x HISTSIZE="1000"
declare -x HOME="/home/icexmoon"
declare -x HOSTNAME="xyz.icexmoon.centos"
declare -x LANG="zh_CN.UTF-8"
[icexmoon@xyz ~]$ export | head -n 5 | cut -c 12-
HISTCONTROL="ignoredups"
HISTSIZE="1000"
HOME="/home/icexmoon"
HOSTNAME="xyz.icexmoon.centos"
LANG="zh_CN.UTF-8"

这个例子用于仅显示全局变量的名称和内容,去掉属性的显示,其中-c可以指定字符长度切割。

grep

之前我们使用过多次了,就是从给定数据中显示出匹配的行:

[icexmoon@xyz ~]$ last | grep icexmoon | tail -n 5
icexmoon tty2                          Thu Jul 29 16:37 - 16:38  (00:00)
icexmoon :0           :0               Thu Jul 29 15:48 - 16:37  (00:48)
icexmoon :0           :0               Thu Jul 29 15:43 - down   (00:04)
icexmoon :0           :0               Tue Jul 27 12:50 - crash (2+02:51)
icexmoon :0           :0               Sat Jul 24 14:49 - crash (2+22:00)

如果要匹配不包含某字符串的行:

[icexmoon@xyz ~]$ last | grep -v icexmoon | tail -n 5
reboot   system boot  3.10.0-1160.el7. Thu Jul 29 15:42 - 15:48  (00:05)
reboot   system boot  3.10.0-1160.el7. Tue Jul 27 12:49 - 15:48 (2+02:58)
reboot   system boot  3.10.0-1160.el7. Sat Jul 24 14:47 - 15:48 (5+01:00)

wtmp begins Sat Jul 24 14:47:46 2021

此外grep是支持正则表达式的,也就是说可以按照正则表达式进行匹配。正则表达式的内容会在后边学习。

排序

sort

sort可以将给定的数据以行为单位进行排序,具体的排序算法会以编码进行比对,所以是和LANG等语言编码环境变量有关的,但是一般而言,对于ASCII字符,所有字符集都是兼容的,不会出现排序结果不一致的情况,但超过ASCII字符集的字符串就很难说了。

[icexmoon@xyz ~]$ cat /etc/passwd | sort | head -n 10
abrt:x:173:173::/etc/abrt:/sbin/nologin
adm:x:3:4:adm:/var/adm:/sbin/nologin
avahi:x:70:70:Avahi mDNS/DNS-SD Stack:/var/run/avahi-daemon:/sbin/nologin
bin:x:1:1:bin:/bin:/sbin/nologin
chrony:x:993:988::/var/lib/chrony:/sbin/nologin
colord:x:997:995:User for colord:/var/lib/colord:/sbin/nologin
daemon:x:2:2:daemon:/sbin:/sbin/nologin
dbus:x:81:81:System message bus:/:/sbin/nologin
ftp:x:14:50:FTP User:/var/ftp:/sbin/nologin
games:x:12:100:games:/usr/games:/sbin/nologin

再看一个例子:

[icexmoon@xyz ~]$ cat /etc/passwd | sort -t ':' -k 3 | head -n 10
root:x:0:0:root:/root:/bin/bash
icexmoon:x:1000:1000:icexmoon:/home/icexmoon:/bin/bash
qemu:x:107:107:qemu user:/:/sbin/nologin
operator:x:11:0:operator:/root:/sbin/nologin
usbmuxd:x:113:113:usbmuxd user:/:/sbin/nologin
bin:x:1:1:bin:/bin:/sbin/nologin
games:x:12:100:games:/usr/games:/sbin/nologin
ftp:x:14:50:FTP User:/var/ftp:/sbin/nologin
pulse:x:171:171:PulseAudio System Daemon:/var/run/pulse:/sbin/nologin
rtkit:x:172:172:RealtimeKit:/proc:/sbin/nologin

这里通过指定分隔符-t ':',以及指定排序区间-k 3,进行排序(类似于先cut -d ':' -f 3后再排序)。

可以看到排序的结果是0<1000<107<11,乍看有问题,但是请记住,着是按字符集编码进行字符串比较,也就是逐个字符比较大小,自然1000小于107。

uniq

uniq用于对给定数据以行为单位进行分组去重,其效果类似于SQL语句中的GROUP:

[icexmoon@xyz ~]$ last | cut -d ' ' -f 1 | sort | uniq

icexmoon
reboot
wtmp
[icexmoon@xyz ~]$

这里的效果是查看登录过系统的用户有哪些,需要注意的是必须先使用sort进行排序,然后再使用uniq进行分组去重,因为uniq本身的逻辑很”简单“,仅会对连续的重复行进行合并,如果中间隔着其它行就不会合并.....

还可以显示重复出现的次数:

[icexmoon@xyz ~]$ last | cut -d ' ' -f 1 | sort | uniq -c
      1
     50 icexmoon
     20 reboot
      1 wtmp
[icexmoon@xyz ~]$

这个结果就相当于统计用户登录的次数。

wc

wc(words count)这个命令用于统计文件中的行数和字符数:

[icexmoon@xyz ~]$ cat /etc/passwd | wc
     43      87    2271

输出的结果分别是:行数、单词数、字符数。

[icexmoon@xyz ~]$ cat /etc/passwd | wc -l
43

使用-l会仅显示行数,上边的示例相当于统计当前系统有多少个账号。

tee

tee这个命令很有意思,它可以在让stdout保持原有指向的同时,重定向到一个文件或程序:

image-20210815182842082

(图片来自鸟哥的私房菜)

这里的图有个瑕疵,screen应该是stdout。

我们看实际使用:

[icexmoon@xyz ~]$ last | cut -d ' ' -f 1 | tail -n 10
icexmoon
reboot
icexmoon
reboot
icexmoon
reboot
icexmoon
reboot

wtmp
[icexmoon@xyz ~]$ last | cut -d ' ' -f 1 | tee /tmp/last_cut | tail -n 10
icexmoon
reboot
icexmoon
reboot
icexmoon
reboot
icexmoon
reboot

wtmp
[icexmoon@xyz ~]$ cat /tmp/last_cut | tail -n 10
icexmoon
reboot
icexmoon
reboot
icexmoon
reboot
icexmoon
reboot

wtmp
[icexmoon@xyz ~]$

通过tee,我们可以将管道连接的命令中的某一个中间结果保存起来,这很像是监听行为,就像是你隔壁家的邻居在打电话,你拉一根电话线去旁接上去,然后连一个录音机记录下通话内容......

这是违法行为,仅用作举例,而且现在的通信也都会加密,当然CIA之流有相应的小工具......

默认情况下tee会对重定向的文件进行覆盖,如果想只追加不覆盖,可以:

[icexmoon@xyz ~]$ wc /tmp/last_cut
 72  71 596 /tmp/last_cut
[icexmoon@xyz ~]$ last | cut -d ' ' -f 1 | tee -a /tmp/last_cut | tail -n 10
icexmoon
reboot
icexmoon
reboot
icexmoon
reboot
icexmoon
reboot

wtmp
[icexmoon@xyz ~]$ wc /tmp/last_cut
 144  142 1192 /tmp/last_cut

字符转换命令

tr

tr用于删除或替换字符串中的指定内容:

[icexmoon@xyz ~]$ last | tr [a-z] [A-Z] | tail -n 10
ICEXMOON :0           :0               THU JUL 29 15:48 - 16:37  (00:48)
REBOOT   SYSTEM BOOT  3.10.0-1160.EL7. THU JUL 29 15:48 - 21:24 (8+05:35)
ICEXMOON :0           :0               THU JUL 29 15:43 - DOWN   (00:04)
REBOOT   SYSTEM BOOT  3.10.0-1160.EL7. THU JUL 29 15:42 - 15:48  (00:05)
ICEXMOON :0           :0               TUE JUL 27 12:50 - CRASH (2+02:51)
REBOOT   SYSTEM BOOT  3.10.0-1160.EL7. TUE JUL 27 12:49 - 15:48 (2+02:58)
ICEXMOON :0           :0               SAT JUL 24 14:49 - CRASH (2+22:00)
REBOOT   SYSTEM BOOT  3.10.0-1160.EL7. SAT JUL 24 14:47 - 15:48 (5+01:00)

WTMP BEGINS SAT JUL 24 14:47:46 2021

将输出结果中的小写字母替换为大写字母。

[icexmoon@xyz ~]$ cat /etc/passwd | tr -d ':' | head -n 10
rootx00root/root/bin/bash
binx11bin/bin/sbin/nologin
daemonx22daemon/sbin/sbin/nologin
admx34adm/var/adm/sbin/nologin
lpx47lp/var/spool/lpd/sbin/nologin
syncx50sync/sbin/bin/sync
shutdownx60shutdown/sbin/sbin/shutdown
haltx70halt/sbin/sbin/halt
mailx812mail/var/spool/mail/sbin/nologin
operatorx110operator/root/sbin/nologin

删除了输出中的指定字符:。

[icexmoon@xyz tmp]$ cat -A passwd
[icexmoon@xyz tmp]$ cp /etc/passwd /tmp/passwd
[icexmoon@xyz tmp]$ unix2dos /tmp/passwd
unix2dos: converting file /tmp/passwd to DOS format ...
[icexmoon@xyz tmp]$ cat -A passwd | head -n 10
root:x:0:0:root:/root:/bin/bash^M$
bin:x:1:1:bin:/bin:/sbin/nologin^M$
daemon:x:2:2:daemon:/sbin:/sbin/nologin^M$
adm:x:3:4:adm:/var/adm:/sbin/nologin^M$
lp:x:4:7:lp:/var/spool/lpd:/sbin/nologin^M$
sync:x:5:0:sync:/sbin:/bin/sync^M$
shutdown:x:6:0:shutdown:/sbin:/sbin/shutdown^M$
halt:x:7:0:halt:/sbin:/sbin/halt^M$
mail:x:8:12:mail:/var/spool/mail:/sbin/nologin^M$
operator:x:11:0:operator:/root:/sbin/nologin^M$
[icexmoon@xyz tmp]$ cat passwd | tr -d '\r' > passwd2
[icexmoon@xyz tmp]$ cat -A passwd2 | head -n 10
root:x:0:0:root:/root:/bin/bash$
bin:x:1:1:bin:/bin:/sbin/nologin$
daemon:x:2:2:daemon:/sbin:/sbin/nologin$
adm:x:3:4:adm:/var/adm:/sbin/nologin$
lp:x:4:7:lp:/var/spool/lpd:/sbin/nologin$
sync:x:5:0:sync:/sbin:/bin/sync$
shutdown:x:6:0:shutdown:/sbin:/sbin/shutdown$
halt:x:7:0:halt:/sbin:/sbin/halt$
mail:x:8:12:mail:/var/spool/mail:/sbin/nologin$
operator:x:11:0:operator:/root:/sbin/nologin$

这里使用tr完成了之前介绍的dos2unix命令的作用,删除了\r字符。

col

col命令可以将制表符转换为空格,这个用途在编程领域还是使用的挺广泛的,比如虽然Python同时支持空格和制表符缩进,但官方规范中推荐写法是空格。

我们看/etc/man_db.conf这个文件:

[icexmoon@xyz tmp]$ cat -A /etc/man_db.conf | head -n 20
# $
#$
# This file is used by the man-db package to configure the man and cat paths.$
# It is also used to provide a manpath for those without one by examining$
# their PATH environment variable. For details see the manpath(5) man page.$
#$
# Lines beginning with `#' are comments and are ignored. Any combination of$
# tabs or spaces may be used as `whitespace' separators.$
#$
# There are three mappings allowed in this file:$
# --------------------------------------------------------$
# MANDATORY_MANPATH^I^I^Imanpath_element$
# MANPATH_MAP^I^Ipath_element^Imanpath_element$
# MANDB_MAP^I^Iglobal_manpath^I[relative_catpath]$
#---------------------------------------------------------$
# every automatically generated MANPATH includes these fields$
#$
#MANDATORY_MANPATH ^I^I^I/usr/src/pvm3/man$
#$
MANDATORY_MANPATH^I^I^I/usr/man$

包含很多制表符(^I),转换为空格:

[icexmoon@xyz tmp]$ cat /etc/man_db.conf | head -n 20 | col -x | cat -A
#$
#$
# This file is used by the man-db package to configure the man and cat paths.$
# It is also used to provide a manpath for those without one by examining$
# their PATH environment variable. For details see the manpath(5) man page.$
#$
# Lines beginning with `#' are comments and are ignored. Any combination of$
# tabs or spaces may be used as `whitespace' separators.$
#$
# There are three mappings allowed in this file:$
# --------------------------------------------------------$
# MANDATORY_MANPATH                     manpath_element$
# MANPATH_MAP           path_element    manpath_element$
# MANDB_MAP             global_manpath  [relative_catpath]$
#---------------------------------------------------------$
# every automatically generated MANPATH includes these fields$
#$
#MANDATORY_MANPATH                      /usr/src/pvm3/man$
#$
MANDATORY_MANPATH                       /usr/man$
[icexmoon@xyz tmp]$

join

join的用途是以行为单位合并数据,其功能类似于SQL中的JOIN,也可以指定”连接主键“。

我们看示例:

[icexmoon@xyz tmp]$ sudo head -n 3 /etc/passwd /etc/shadow
[sudo] icexmoon 的密码:
==> /etc/passwd <==
root:x:0:0:root:/root:/bin/bash
bin:x:1:1:bin:/bin:/sbin/nologin
daemon:x:2:2:daemon:/sbin:/sbin/nologin

==> /etc/shadow <==
root:$6$h5eXZCiKw8ucXyB2$PxxRgKneIDs2TWIpg916ugAfGCfqNf/HJHiXjy0PTPtUmY.pR/e2cUMpjRBqbx5Y0AHNJtjxAf3aOUTek3DpO.::0:99999:7:::
bin:*:18353:0:99999:7:::
daemon:*:18353:0:99999:7:::
[icexmoon@xyz tmp]$ sudo join -t ':' /etc/passwd /etc/shadow | head -n 3
root:x:0:0:root:/root:/bin/bash:$6$h5eXZCiKw8ucXyB2$PxxRgKneIDs2TWIpg916ugAfGCfqNf/HJHiXjy0PTPtUmY.pR/e2cUMpjRBqbx5Y0AHNJtjxAf3aOUTek3DpO.::0:99999:7:::
bin:x:1:1:bin:/bin:/sbin/nologin:*:18353:0:99999:7:::
daemon:x:2:2:daemon:/sbin:/sbin/nologin:*:18353:0:99999:7:::
[icexmoon@xyz tmp]$

/etc/passwd和/etc/shadow两个文件都是按:进行分隔数据的,且第一个字段都是用户名,所以我们可以使用join进行合并,通过-t ':'指定分隔符,join默认使用第一个字段作为”主键“进行合并。

我们看一个复杂点的例子:

[icexmoon@xyz tmp]$ head -n 3 /etc/passwd /etc/group
==> /etc/passwd <==
root:x:0:0:root:/root:/bin/bash
bin:x:1:1:bin:/bin:/sbin/nologin
daemon:x:2:2:daemon:/sbin:/sbin/nologin

==> /etc/group <==
root:x:0:
bin:x:1:
daemon:x:2:
[icexmoon@xyz tmp]$ join -t ':' -1 4 /etc/passwd -2 3 /etc/group | head -n 3
join: /etc/passwd:6: is not sorted: sync:x:5:0:sync:/sbin:/bin/sync
join: /etc/group:11: is not sorted: wheel:x:10:icexmoon
0:root:x:0:root:/root:/bin/bash:root:x:
1:bin:x:1:bin:/bin:/sbin/nologin:bin:x:
2:daemon:x:2:daemon:/sbin:/sbin/nologin:daemon:x:

这个例子中/etc/passwd的第四个字段是GID,/etc/group第三个字段也是GID,在这里我们将GID作为主键进行合并,其中-1 4表明使用第一个文件中的第四个字段,-2 3表明使用第二个文件的第三个字段。

需要注意的是,合并时候两个数据中的主键必须逐行一一对应,如果不是的话可能需要按主键进行排序后再合并。

paste

paste命令可以将两个数据粘贴到一起,使用制表符分隔:

[icexmoon@xyz tmp]$ sudo paste /etc/passwd /etc/shadow | head -n 3
root:x:0:0:root:/root:/bin/bash root:$6$h5eXZCiKw8ucXyB2$PxxRgKneIDs2TWIpg916ugAfGCfqNf/HJHiXjy0PTPtUmY.pR/e2cUMpjRBqbx5Y0AHNJtjxAf3aOUTek3DpO.::0:99999:7:::
bin:x:1:1:bin:/bin:/sbin/nologin        bin:*:18353:0:99999:7:::
daemon:x:2:2:daemon:/sbin:/sbin/nologin daemon:*:18353:0:99999:7:::

再看复杂一点的例子:

[root@xyz ~]# cat /etc/group | paste /etc/passwd /etc/shadow - | head -n 3
root:x:0:0:root:/root:/bin/bash root:$6$h5eXZCiKw8ucXyB2$PxxRgKneIDs2TWIpg916ugAfGCfqNf/HJHiXjy0PTPtUmY.pR/e2cUMpjRBqbx5Y0AHNJtjxAf3aOUTek3DpO.::0:99999:7:::        root:x:0:
bin:x:1:1:bin:/bin:/sbin/nologin        bin:*:18353:0:99999:7:::        bin:x:1:
daemon:x:2:2:daemon:/sbin:/sbin/nologin daemon:*:18353:0:99999:7:::     daemon:x:2:

这个例子中paste将两个文件以及前一个管道命令的stdout粘贴到了一起,其中-代表前一个管道命令的stdout,也就是当前管道命令的stdin,本来paste命令后连接多个文件的时候是没法处理stdin的数据的,但是我们使用-进行占位就可行了。

expand

与col -x类似,expand命令同样可以将\t转化为空格:

[root@xyz ~]# grep '^MANPATH' /etc/man_db.conf | head -n 3
MANPATH_MAP     /bin                    /usr/share/man
MANPATH_MAP     /usr/bin                /usr/share/man
MANPATH_MAP     /sbin                   /usr/share/man
[root@xyz ~]# grep '^MANPATH' /etc/man_db.conf | head -n 3 |cat -A
MANPATH_MAP^I/bin^I^I^I/usr/share/man$
MANPATH_MAP^I/usr/bin^I^I/usr/share/man$
MANPATH_MAP^I/sbin^I^I^I/usr/share/man$
[root@xyz ~]# grep '^MANPATH' /etc/man_db.conf | head -n 3 | expand -t 6 - |cat -A
MANPATH_MAP /bin              /usr/share/man$
MANPATH_MAP /usr/bin          /usr/share/man$
MANPATH_MAP /sbin             /usr/share/man$

默认为将一个制表符转化为8个空格,但我们可以通过-t 6进行修改,转换为指定个数个空格。

此外unexpand命令可以将空格转换为制表符。

split

如果你有一些比较大的文本文件,想进行切割保存或传输,可以使用split命令:

[root@xyz ~]# cd /tmp
[root@xyz tmp]# split -b 300k /etc/services services
[root@xyz tmp]# ls -al services*
-rw-r--r--. 1 root root 307200 8月  15 19:27 servicesaa
-rw-r--r--. 1 root root 307200 8月  15 19:27 servicesab
-rw-r--r--. 1 root root  55893 8月  15 19:27 servicesac

这里使用参数-b 300k将/etc/services切割为了不超过300k的三块文件进行保存,并且以xxxaa、xxxab、xxxac这样的方式进行命名。

如果想合并回去,可以:

[root@xyz tmp]# cat services* > services.bak
[root@xyz tmp]# wc -l services.bak /etc/services
  11176 services.bak
  11176 /etc/services
  22352 总用量
[root@xyz tmp]# ls -al services.bak /etc/services
-rw-r--r--. 1 root root 670293 6月   7 2013 /etc/services
-rw-r--r--. 1 root root 670293 8月  15 19:28 services.bak
[root@xyz tmp]#

除了按大小进行切割,还可以按行进行切割:

[root@xyz tmp]# rm services*
[root@xyz tmp]# split -l 2000 /etc/services services
[root@xyz tmp]# ls -al services*
-rw-r--r--. 1 root root 114651 8月  15 19:32 servicesaa
-rw-r--r--. 1 root root 115687 8月  15 19:32 servicesab
-rw-r--r--. 1 root root 113266 8月  15 19:32 servicesac
-rw-r--r--. 1 root root 123544 8月  15 19:32 servicesad
-rw-r--r--. 1 root root 127692 8月  15 19:32 servicesae
-rw-r--r--. 1 root root  75453 8月  15 19:32 servicesaf
[root@xyz tmp]# wc -l services*
  2000 servicesaa
  2000 servicesab
  2000 servicesac
  2000 servicesad
  2000 servicesae
  1176 servicesaf
 11176 总用量
[root@xyz tmp]#

xargs

xargs可以用空白符或者换行对stdin的数据进行切割,然后将切割后的字段作为参数传递给命令进行执行:

[root@xyz tmp]# cut -d ':' -f 1 /etc/passwd | head -n 3
root
bin
daemon
[root@xyz tmp]# id root
uid=0(root) gid=0(root) 组=0(root)
[root@xyz tmp]# cut -d ':' -f 1 /etc/passwd | head -n 3 | id
uid=0(root) gid=0(root) 组=0(root) 环境=unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023
[root@xyz tmp]# cut -d ':' -f 1 /etc/passwd | head -n 3 | xargs id
id: 额外的操作数 "bin"
Try 'id --help' for more information.
[root@xyz tmp]# cut -d ':' -f 1 /etc/passwd | head -n 3 | xargs -n 1 id
uid=0(root) gid=0(root) 组=0(root)
uid=1(bin) gid=1(bin) 组=1(bin)
uid=2(daemon) gid=2(daemon) 组=2(daemon)
[root@xyz tmp]#

命令cut -d ':' -f 1 /etc/passwd | head -n 3可以打印三个账号,命令id root可以打印账号相关的uid和gid,如果我们想把这两个命令连起来,通过id直接打印三个账号的信息,使用cut -d ':' -f 1 /etc/passwd | head -n 3 | id这样的命令是不行的,原因在于id仅能处理单个账号,一次性将换行符连接的三个账号名给它,它并不能很好的同时输出三个账号的信息。所以我们要借助xargs,使用下面这样的最终命令:

cut -d ':' -f 1 /etc/passwd | head -n 3 | xargs -n 1 id

在这个命令中xargs -n 1 id将stdin传入的换行符分隔的三个账号名进行切割,变成三个账号名字符串,然后-n 1意味着xargs会逐次调用id命令,且每次只会传递一个字符串作为id的参数,也就是说会依次执行id root; id bin; id daemon。

如果xargs关联的是某些敏感操作,逐次调用的时候需要询问用户进行确认,可以:

[root@xyz tmp]# cut -d ':' -f 1 /etc/passwd | head -n 3 | xargs -p -n 1 id
id root ?...y
uid=0(root) gid=0(root) 组=0(root)
id bin ?...y
uid=1(bin) gid=1(bin) 组=1(bin)
id daemon ?...y
uid=2(daemon) gid=2(daemon) 组=2(daemon)

这样就很清楚了,的确是逐次调用id命令。

除此之外,xargs还可以将某些不支持管道命令的命令整合进管道命令:

[root@xyz tmp]# find /usr/sbin -perm /7000
/usr/sbin/unix_chkpwd
/usr/sbin/pam_timestamp_check
/usr/sbin/lockdev
/usr/sbin/userhelper
/usr/sbin/netreport
/usr/sbin/usernetctl
/usr/sbin/mount.nfs
/usr/sbin/postdrop
/usr/sbin/postqueue
[root@xyz tmp]# find /usr/sbin -perm /7000 | ls -ald
drwxrwxrwt. 32 root root 4096 8月  15 19:33 .
[root@xyz tmp]# find /usr/sbin -perm /7000 | xargs ls -ald
-rwx--s--x. 1 root lock      11208 6月  10 2014 /usr/sbin/lockdev
-rwsr-xr-x. 1 root root     117432 10月  1 2020 /usr/sbin/mount.nfs
-rwxr-sr-x. 1 root root      11224 10月 13 2020 /usr/sbin/netreport
-rwsr-xr-x. 1 root root      11232 4月   1 2020 /usr/sbin/pam_timestamp_check
-rwxr-sr-x. 1 root postdrop 218560 4月   1 2020 /usr/sbin/postdrop
-rwxr-sr-x. 1 root postdrop 264128 4月   1 2020 /usr/sbin/postqueue
-rwsr-xr-x. 1 root root      36272 4月   1 2020 /usr/sbin/unix_chkpwd
-rws--x--x. 1 root root      40328 8月   9 2019 /usr/sbin/userhelper
-rwsr-xr-x. 1 root root      11296 10月 13 2020 /usr/sbin/usernetctl
[root@xyz tmp]#

ls命令是不支持stdin的,所以find /usr/sbin -perm /7000 | ls -ald无法正常工作,但使用xargs之后就可以在管道命令中正常工作了。

-的作用

在管道命令中-可以代替stdin或者stdout作为命令参数的占位符:

[root@xyz tmp]# mkdir /tmp/homeback
[root@xyz tmp]# tar -cvf - /home | tar -xvf - -C /tmp/homeback
tar: 从成员名中删除开头的“/”
/home/
/home/icexmoon/
/home/icexmoon/.mozilla/
/home/icexmoon/.mozilla/extensions/
/home/icexmoon/.mozilla/plugins/
/home/icexmoon/.mozilla/firefox/
/home/icexmoon/.mozilla/firefox/p1dll8kw.default-default/
/home/icexmoon/.mozilla/firefox/p1dll8kw.default-default/.parentlock
/home/icexmoon/.mozilla/firefox/p1dll8kw.default-default/compatibility.ini
/home/icexmoon/.mozilla/firefox/p1dll8kw.default-default/permissions.sqlite
home/
...省略...
/home/icexmoon/tmp.tar.log
home/icexmoon/tmp.bak.log
home/icexmoon/tmp.tar.log
[root@xyz tmp]# ls -ald /tmp/homeback/home /home
drwxr-xr-x. 3 root root 22 7月  24 14:45 /home
drwxr-xr-x. 3 root root 22 7月  24 14:45 /tmp/homeback/home

tar -cvf - /home | tar -xvf - -C /tmp/homeback相当有意思,在第一个管道命令中-代表stdout,也就是将/home进行打包后将打包文件输出到stdout。在第二个命令中-代表stdin,也就是说从stdin获取打包文件,然后进行解包,然后输出到/tmp/homeback目录。

结果相当于使用cp,但是其过程与cp是完全不同的,经历了打包和解包。

关于Bash的内容介绍完毕,谢谢阅读。

本作品采用 知识共享署名 4.0 国际许可协议 进行许可
标签: bash Linux sh
最后更新:2021年8月15日

魔芋红茶

加一点PHP,加一点Go,加一点Python......

点赞
< 上一篇
下一篇 >

文章评论

取消回复

*

code

COPYRIGHT © 2021 icexmoon.cn. ALL RIGHTS RESERVED.
本网站由提供CDN加速/云存储服务

Theme Kratos Made By Seaton Jiang

宁ICP备2021001508号

宁公网安备64040202000141号