Linux学习笔记(九):Bash

  • Bash是什么
  • Shell变量
  • 定义与使用
  • 环境变量
  • 其他指令
  • 变量内容修改(高级)
  • 指令别名
  • Bash操作环境
  • 数据流重导向
  • 管线命令


Bash是什么

Shell是用户与Linux内核交互的接口,它既是一种命令语言,也是一种程序设计语言。相比图形界面,使用shell的传输速度更快,不容易出现断线或者信息外流的问题。Shell有许多不同的版本,比如Bourne Shell (sh)、C Shell、TCSH、K Shell。Linux目前使用的是Bourne Again Shell,简称为bash,是Bourne Shell的增强版本。文件 /etc/shells 中描述了系统中支持的shell版本,/etc/passwd 中则记录了每个用户登录后使用的预设shell。

下面介绍一些在终端中使用shell的技巧:

  • 使用bash时,按下tab键可以进行命令或文本补全;
  • 输入 type [-t] command 查看命令command是否为外部指令 (file)、命令别名 (alias) 或者bash内建指令 (builtin);
  • 当输入的指令很长时,输入反斜杠 \ 可以进行指令换行;
  • 按下Ctrl u可以删除光标前的所有指令串,Ctrl k则可以向后删除所有指令串;
  • 按下Ctrl a可以让光标移动到指令串最前面,Ctrl e则可以移动到最后面。

Shell变量

shell变量可以分为环境变量和用户自定义变量。环境变量一般都大写表示,比如PATH、HOME、MAIL、SHELL等。

定义与使用

变量使用等号定义,且必须满足以下规则:

  • 等号两边不能直接接空格;
  • 变量名不能以数字开头,且只能包括英文字母与数字;
  • 变量内容中如果有空格可以使用单引号或双引号将其包含起来。使用单引号时,变量内容中以 $ 开头的特殊字符会被转化为一般文本,失去其特殊意义;
  • 变量内容中使用斜杠 \ 可以将特殊字符(空格、换行符、$、\、引号等)变成一般文本字符;
  • 在长指令串中可以包含子指令串,但必须用 反单引号`` 或者 $() 包围起来;
  • 使用变量内容的格式为 $变量名 或者 ${变量名},还可以使用 “$变量名” 或者 ${变量名} 来扩增变量内容;
  • 使用export命令可以将一般变量变为环境变量,以供子程序使用;
  • 使用unset命令可以取消变量定义。
firstname=Lebron
playername=LebronJames
playername=Lebron\ James
playername="Lebron James"
playername='Lebron James'
playername="${firstname} James"
echo ${playername}   # 显示Lebron James
playername='${firstname} James'
echo $playername # 显示${firstname} James

version=`uname -r`
echo ${version}   # 显示Linux内核版本
version=$(uname -r)
echo $version
unset version   # 取消变量
echo $version   # 输出为空

echo ${PATH}
PATH="$PATH":/home/bin
PATH=${PATH}:/home/bin
echo $PATH
export PATH

环境变量

env
使用env指令可以查看shell环境下的所有环境变量。其中一些重要的环境变量包括:

  • HOME:用户的家目录;
  • SHELL:当前环境使用的shell版本;
  • HISTSIZE:系统记录的历史指令的条数;
  • PATH:执行文件的搜寻路径;
  • LANG:语系信息,与文件编码和字符排序有关;
  • RANDOM:随机数生成器。

set
set指令可以显示bash环境内的所有变量,包括环境变量。其中,有如下几个特殊变量:

  • PS1:命令提示字符设定,即在终端中输入指令前的那一串字符,形如 [用户名@主机名 工作目录]$,比如[root@xxx home]#
  • $:当前shell的进程号PID(echo $$ 查看);
  • ?:上一个指令执行后的返回值,一般等于0,表示成功执行(echo $? 查看)。

export
在已经打开的shell终端中执行bash指令可以打开一个bash子程序,输入exit可以退出子程序。bash子程序只会继承bash父程序中的环境变量。如果想在子程序中使用父程序中自定义的变量,需要先使用export指令将自定义变量变成环境变量。

其他指令

read
使用read指令可以从键盘读入数据并存入变量(不需要事先定义变量)。该指令常用于shell脚本。指令格式为 read -p "提示语" -t 秒数 变量名,其中秒数为等待执行者输入的时间。

declare
使用declare指令可以设置变量类型。

sum=50+100+150
echo $sum   # 输出50+100+150
declare -i sum=50+100+150   # -i表示整数
echo $sum   # 输出300

declare -x sum   # -x表示设为环境变量
declare -r sum   # -r表示设为只读变量
declare +x sum   # +x表示取消环境变量(加号表示取消)
declare -a arr   # -a表示数组
declare -p arr   # -p表示列出变量的类型

array
定义数组时直接给元素赋值即可。

var[1]=Bronny
var[2]=Maximus
var[3]=Julie
echo "${var[1]}, ${var[2]}, ${var[3]}"

ulimit
ulimit指令用于限制当前shell环境中用户能够使用的系统资源,包括可以打开的文件数目、可以使用的CPU时间以及内存大小。

变量内容修改(高级)

变量内容的删除与替换

变量设定方式

说明

${变量#关键词}

若变量内容从头开始的数据与关键词匹配,删除匹配的最短数据

${变量##关键词}

若变量内容从头开始的数据与关键词匹配,删除匹配的最长数据

${变量%关键词}

若变量内容从尾向前的数据与关键词匹配,删除匹配的最短数据

${变量%%关键词}

若变量内容从尾向前的数据与关键词匹配,删除匹配的最长数据

${变量/旧字符串/新字符串}

若变量内容与旧字符串匹配,将第一个旧字符串替换为新字符串

${变量//旧字符串/新字符串}

若变量内容与旧字符串匹配,将所有的旧字符串替换为新字符串

变量的测试与内容修改

变量设定方式

str没有设定

str为空字符

str设定为非空字符

var=${str-expr}

var=expr

var=

var=$str

var=${str:-expr}

var=expr

var=expr

var=$str

var=${str+expr}

var=

var=expr

var=expr

var=${str:+expr}

var=

var=

var=expr

var=${str=expr}

str=expr, var=expr

str不变, var=

str不变, var=$str

var=${str:=expr}

str=expr, var=expr

str=expr, var=expr

str不变, var=$str

var=${str?expr}

expr输出至stderr

var=

var=$str

var=${str:?expr}

expr输出至stderr

expr输出至stderr

var=$str

指令别名

alias, unalias
alias指令可以设定指令的别名,可以用来简化经常使用但是比较长的指令。

[fang@myhost ~]$ alias   # 查看目前已经存在的别名
alias ls='ls --color=auto'
alias rm='rm -i'
alias vi='vim'
...
[fang@myhost ~]$ alias ll='ls -l --color=auto'   # 定义别名
[fang@myhost ~]$ ll /home/fang   # 使用别名
[fang@myhost ~]$ unalias ll   # 取消别名

history
使用history指令可以查看执行过的历史指令,指令后接数字n可以查看最近执行过的n条指令。使用 ! 数字!! 可以执行历史指令。

[fang@myhost ~]$ history 3
67 ls   # 指令前的数字表示该指令在当前shell中是第几条历史指令
68 history
69 history 3
[fang@myhost ~]$ !68   # 执行第68条指令:history
[fang@myhost ~]$ !!   # 执行上一条指令,相当于按向上箭头再按Enter

Bash操作环境

指令搜寻顺序
在终端中执行一个指令时,系统按如下顺序搜寻并执行指令:

  1. 以绝对或相对路径执行指令,比如/bin/ls./ls
  2. 由alias别名找到该指令;
  3. 由bash内建的指令执行;
  4. 通过环境变量$PATH中的路径顺序搜索到第一个指令来执行。

环境配置文件
Linux系统中的shell可以分为login shell和non-login shell,它们使用的环境配置文件不相同。login shell需要输入账户名和密码登录才能启动,比如由终端tty1~tty6登录后启动的shell;non-login shell则不需要账户名和密码登录就能使用,比如使用X Windows图形化界面打开的终端、以及在已经启动的终端中执行bash命令打开的子程序

login shell一般会读取以下两个配置文件:

  • /etc/profile
  • ~/.bash_profile 或 ~/.bash_login 或 ~/.profile

non-login shell仅会读取 ~/.bashrc 文件。

终端环境设定
在Linux开机登录界面,按下Ctrl Alt F1~F6可以分别打开tty1~tty6对应的纯命令行交互界面。使用stty指令可以展示终端中某些特殊按键代表的意义。set指令可以用于设定指令输入/输出环境。

通配符
bash环境中的通配符(wildcard)与正则表达式有一些区别。

符号

说明

*

代表0到无穷多个任意字符

?

代表有且仅有一个任意字符

[ ]

代表一定有一个在括号内的字符,比如[ab5]

[-]

代表一个连续的范围,比如[0-9], [a-z]

[^]

代表一定有一个字符,且不在括号中,比如[^abc]

数据流重导向

标准输出、标准错误输出、标准输入
标准输出(STDOUT)是指指令执行成功后返回的信息。标准错误输出(STDERR)是指指令执行失败后返回的错误信息。标准输入(STDIN)一般是指由键盘(或者文件)输入的数据。标准输出和标准错误输出默认都会显示到屏幕上。数据流重导向可以将它们传送到其他文件或者设备,其规则为:

  • STDIN:代码为0,重导向符号使用 < 或 <<;
  • STDOUT:代码为1,重导向符号使用 > 或 >>;
  • STDERR:代码为2,重导向符号使用 2> 或 2>>;
  • 可以使用 2>&1 或 &> 将STDOUT和STDERR同时重定向到同一个文件。

如果重导向符号后的文件名不存在,则会被自动创建。符号 > 与 >> 的区别在于前者会覆盖文件中已有的内容,而后者会将输出累加到文件中已有的数据之后

/dev/null 黑洞
如果想忽略输出的内容,可以将输出重定向到/dev/null。重定向到 /dev/null 的所有数据都会被丢弃

[fang@myhost ~]$ find /home -name .bashrc > filename1 2> filename2
[fang@myhost ~]$ find /home -name .bashrc 2> /dev/null

[fang@myhost ~]$ find /home -name .bashrc > filename 2> filename   # 错误
[fang@myhost ~]$ find /home -name .bashrc > filename 2>&1   # 正确
[fang@myhost ~]$ find /home -name .bashrc &> filename   # 正确

标准输入:< 与 <<
使用 < 符号可以将标准输入由键盘改为文件,而 << 符号可用于指定键盘输入的结束字符。

[fang@myhost ~]$ cat > myfile
testing myfile ...   # 由键盘输入
# 按Ctrl d 结束键盘输入
[fang@myhost ~]$ cat myfile
testing myfile ...

[fang@myhost ~]$ cat > myfile < ~/.bashrc   # 由文件输入
[fang@myhost ~]$ cat > myfile << "eof"
> new testfile ...
> eof   # 不用输入Ctrl d
[fang@myhost ~]$ cat myfile
new testfile ...

命令执行的判断依据 ; && ||
如果需要连续执行多条指令时,可以使用分号隔开指令。如果不同指令之间存在相关性,则可以使用&&或||进行判断。如果有多个判断符号,从左至右依次判断。

指令

说明

cmd1 && cmd2

只有当cmd1成功执行时才会执行cmd2

cmd1 || cmd2

只有当cmd1执行出错时才会执行cmd2

cmd1 && cmd2 || cmd3

如果cmd1执行成功,则仅执行cmd2;

如果cmd1执行失败,则仅执行cmd3

cmd1 || cmd2 && cmd3

如果cmd1执行成功,则仅执行cmd3;

如果cmd1执行失败,则先执行cmd2,cmd2执行成功才会执行cmd3

管线命令

管线(pipeline)命令 | 能够将左边指令的标准输出STDOUT转变为标准输入STDIN并传递给右边的指令。下面介绍一些支持管线命令的常用指令。

cut, grep
cut指令能够以行为单位切分数据并取出其中特定位置的内容,指令格式为cut -d '分隔符' -f 位置cut -c 字符区间。grep指令则能够以行为单位搜寻给定的字符串,指令格式为grep [-acinv] [--color=auto] '字符串' 文件名

sort, wc, uniq
sort用于排序,排序结果与语系编码有关,指令格式为sort [-fbMnrtuk] 文件名或STDIN。uniq用于取消显示排序结果中的重复项,指令格式为uniq [-ic]。wc指令用于显示文件包含的行数或字符数,指令格式为wc [-lwm]

tee
tee指令用于双向重导向,可以同时将数据流导向给文件和STDOUT(即屏幕)。指令格式为tee [-a] 文件名

tr, col, join, paste, expand
tr指令用于删除或替换数据流中的文字,指令格式为tr [-ds] '字符串'。col指令一般用于将tab键转变为对等的空格键,指令格式为col -x。join指令用于合并两个文件中有相同内容的行,其语法为join [-ti12] 文件1 文件2使用join指令前必须对文件进行排序。paste指令用于直接将两个文件的行合并(默认以tab键分隔),而不会比较文件的内容,其语法为paste [-d] 文件1 文件2。expand指令也可以将tab键转变成空格,指令格式为expand [-t] 文件名

split
split指令可以将一个大文件分区为多个小文件,以便于复制。其指令格式为split [-bl] 大文件名 分区文件名前导符

xargs
xargs指令可以读入STDIN数据流,将其转化为指令参数,从而使得某些原本不支持管线命令的指令可以使用STDIN(比如ls指令)。其语法为xargs [-0epn] command

减号-
在管线命令中,经常会将前一个指令的STDOUT作为后一个指令的STDIN,某些指令需要用到文件名来进行处理时(比如tar),该STDIN与STDOUT可以用减号-替代。

[fang@myhost ~]$ last   # 显示登录账户信息
fang	pts/1	192.168.101.25	Sat	Feb 7 12:35 still logged in
root	pts/1	192.168.101.23	Sat	Feb 7 12:13 - 18:46 (06:33)
...
[fang@myhost ~]$ last | cut -d ' ' -f 1
fang
root
... 

[fang@myhost ~]$ last | grep 'root'   # 取出包含root的行
[fang@myhost ~]$ last | grep -v 'root'   # 取出不包含root的行
[fang@myhost ~]$ last | grep 'root' | cut -d ' ' -f 1
[fang@myhost ~]$ grep --color=auto 'MANPATH' /etc/man_db.conf

[fang@myhost ~]$ cat /etc/passwd | sort   # 默认以tab为分隔符,第一个数据为排序依据
[fang@myhost ~]$ cat /etc/passwd | sort -t ':' -k 3   # 以:作为分隔符,第3个数据为排序依据
[fang@myhost ~]$ last | cut -d ' ' -f 1 | sort

[fang@myhost ~]$ [fang@myhost ~]$ last | cut -d ' ' -f 1 | sort | uniq   # 排序去重
[fang@myhost ~]$ last | cut -d ' ' -f 1 | sort | uniq -c   # 对重复结果计数

[fang@myhost ~]$ cat /etc/man_db.conf | wc   # 文件计数
131	723	5171   # 依次为行数、字数(英文单字)、字符数
[fang@myhost ~]$ last | grep [a-zA-Z] | grep -v 'wtmp' | grep -v 'reboot' | \
> grep -v 'unknown' | wc -l   # 统计去除wtmp、reboot、unknown和空白行之后的用户登录人次

[fang@myhost ~]$ last | tee accfile | cut -d ' ' -f1   # 将数据流保存到文件的同时也显示到屏幕
[fang@myhost ~]$ ls -l /home | tee ~/homefile | more

[fang@myhost ~]$ last | tr '[a-z]' '[A-Z]'   # 将last输出中的小写字母替换为大写字母之后再显示
[fang@myhost ~]$ cat /etc/passwd | tr -d ':'   # 在文件输出的数据中去掉冒号再显示
[fang@myhost ~]$ cat ~/passwd | tr -d '\r' > ~/passwd.linux   # 删除文件中的DOS断行符CRLF后转存为新文件

[fang@myhost ~]$ cat -A /etc/man_db.conf   # 显示文件中所有特殊字符
[fang@myhost ~]$ cat /etc/man_db.conf | col -x | cat -A | more   # 将tab键^I变成空格键再输出

# 使用join指令前先用sort对文件进行排序
[fang@myhost ~]$ join -t ':' /etc/passwd /etc/shadow | head -n 3   
# 以冒号为分隔符(默认是空格),合并两个文件中第一个字段相同的行,并显示前三行
[fang@myhost ~]$ join -t ':' -1 4 /etc/passwd -2 3 /etc/group | head -n 3
# 以冒号为分隔符,合并第一个文件中第4个字段与第二个文件中第3个字段相同的行,把相同的字段放在每行输出内容的最前面,并显示前三行

[fang@myhost ~]$ paste /etc/passwd /etc/shadow   # 以tab键为分隔符合并两个文件的行再显示

# 将/etc/services保存为多个300k大小的文件,文件名前缀为services
[fang@myhost ~]$ cd /tmp; split -b 300k /etc/services services   
[fang@myhost tmp]$ ll -k services*
-rw-rw-r--	1	fang	fang	307200	Jul 9 22:52	servicesaa
-rw-rw-r--	1	fang	fang	307200	Jul 9 22:52	servicesab
-rw-rw-r--	1	fang	fang	 55893	Jul 9 22:52	servicesac
[fang@myhost tmp]$ cat services* >> servicesback   # 合并多个文件

[fang@myhost ~]$ id root 
uid=0(root)	gid=0(root)	groups=0(root)
# 取出/etc/passwd文件中前三行的第一栏(账户名),使用id依次显示每个账号uid等信息
[fang@myhost ~]$ cut -d ':' -f 1 /etc/passwd | head -n 3 | xargs -n 1 id
# 以下是几种错误做法
[fang@myhost ~]$ id $(cut -d ':' -f 1 /etc/passwd | head -n 3)   # id只能接受一个参数
[fang@myhost ~]$ cut -d ':' -f 1 /etc/passwd | head -n 3 | id   # id不是管线命令
[fang@myhost ~]$ cut -d ':' -f 1 /etc/passwd | head -n 3 | xargs id   # id只能接受一个参数

# 依次显示/etc/passwd中每个账户的uid信息,直到遇到sync字符串就停止
[fang@myhost ~]$ cut -d ':' -f 1 /etc/passwd | xargs -e'sync' -n 1 id
# 找到/usr/sbin下具有特殊权限的文件,用ls指令列出详细信息
[fang@myhost ~]$ find /usr/sbin -perm /7000 | xargs ls -l

[fang@myhost ~]$ mkdir /tmp/homeback
# 将/home打包后传给STDOUT,经过管线后变成后面指令的STDIN
[fang@myhost ~]$ tar -cvf - /home | tar -xvf - -C /tmp/homeback