计算机 · 2021年12月8日 0

Shell基础

基础知识

一些个人观点:

  • shell是程序员和Linux/Unix操作系统进行交互的最常用的命令行工具,学会了写shell我们也就学会了如何更好地控制和理解Linux/Unix操作系统;
  • shell是一门解释性脚本语言,它的优点就是作为各种命令行工具的胶水语言用几行简单的代码完成任务;
  • shell的另一个优点是Linux/Unix都会自带这个工具,而python则可能需要自己去手动安装一遍;
  • shell的缺点是运行速度慢,每执行一个命令shell都需要fork一个子进程,如果脚本里有大量的命令调用,那么会多出来很多性能消耗;
  • shell的另一个缺点是难以胜任大型项目或者复杂的项目(也可能是我太菜)。对于大型项目我们需要考虑模块复用,代码调试,这些shell都没有。shell的语法也比较乱,你需要记住各种规则,稍不留心就写错;shell调用各种命令行工具时很多时候都是在使用字符串作为输入输出,而不能用二进制的数据结构,这也导致各个模块之间的通信很麻烦。异常处理似乎也没有;
  • 简单的任务(就是你立马能想到该怎么写每一行的)用shell,复杂的任务考虑把任务进行拆解,先用perl/python等更完备的语言把模块或者子任务写好,然后再用shell把他们串起来;
  • 考虑到语法的通用性,个人觉得学下Bash(GNU Bourne-Again Shell)的语法就可以了,要是Zsh用熟了,登录到服务器上时老是发现这个命令那个命令用不了,应该挺难受的;更复杂的事情可以交给python/perl等脚本语言做,不用在shell里强求;总之我是觉得shell没必要花太多时间去学全和学深入,把宝贵的时间留给更有价值的C++、Linux内核和算法等领域上去;

切入正题:

用一行代码运行多个命令

如果要多个命令一起运行,可在同一提示行输入,用分号隔开:

date ; who

创建shell脚本文件

在创建shell脚本文件时,必须在文件的第一行指定要使用的shell,其格式为:

#!/bin/bash

打印信息

  • 可以使用echo命令输出消息。echo命令后的字符串可以不用加引号,但是如果你想输出的字符串里包含了单引号或者双引号,那么你就需要用另外一种引号把字符串给圈起来。如果你需要字符串的末尾有空格存在,那么也需要使用引号。
    如果希望echo输出信息后不要换行,那么可以使用-n选项。如:
echo -n "The time and date are: "
  • 除了echo还可以使用printf来打印信息。

使用变量

  • 环境变量 可以通过set命令来显示一份完整的活动的环境变量列表。
    使用变量则只需要在变量前加上美元符$即可。有时也会使用)即可。有时也会使用${variable}的格式,这是为了显示指定美元符后的变量名。
    如果想获得字面上的美元符,可以通过\进行转义。
  • 用户变量 用户可以在shell脚本中定义和使用自己的变量。用户变量可以是不超过20个字母,数字或下划线的文本字符串。用户变量区分大小写。
    值通过等号赋给用户变量。在变量,等号和值之间不能出现空格
    shell脚本会自动决定变量值的数据类型。
    引用用户变量的方法和环境变量一样。
  • 更改字段分隔符 bash shell默认的字段分隔符IFS(internal field separator)包括空格、制表符和换行符。如果要更改IFS使其只识别换行符,需要如下设置:IFS=$'\n' 如果要改为用冒号分隔:IFS=: 如果改为用换行符、冒号、分号、和双引号作为分隔字段,可以如下设置:IFS=$'\n:;"' 大概在将IFS设为字符串时需要使用$符号吧,待验证。
  • 上个命令的退出状态码:$?
  • 当前进程的PID:$$

设置PROMPT_COMMAND

之前师兄在我司实习的时候给服务器装上了Oh My Zsh,其中一个主题的效果是不时的会在终端里输出一两句名人名言,特别有意思。我当时想,如果能改为输出自己单词本里的单词,那岂不是可以在敲命令的时候还顺带背两个单词?带着这样的想法我开始考虑怎么在Bash中实现这个功能。

  • 首先,我并不希望切换到Zsh,Zsh确实是很酷、很geek的东西,但是却不如Bash通用,我并不想花时间去学习Zsh那些不通用的特性。
  • 那么认真考虑在Bash中实现这种功能的方法,大概只有修改PS1这种值了。然而这个方法是行不通的,因为PS1的值在载入*.bashrc*脚本之后其实就固定不变了,达不到输出不同单词和时候输出单词有时候不输出单词的效果。后来在Stack Overflow上才看见有人提到PROMPT_COMMAND,Bash会在每次输出命令提示符之前运行PROMPT_COMMAND里的内容。于是可以确认我的想法是切实可行的了。
  • 最终还是放弃了这个想法的具体实现,主要是因为实现这种功能还是需要写一点Bash代码的,然后需要考虑的事情也比较多,比如单词输出的样式要好看(这个特别重要)才行,不要太频繁的在Bash里输出单词,这会影响工作以及Bash性能,以及如果某个程序依赖你的Bash输出,但是你的Bash输出里却夹杂了这种奇怪的东西,那么会很头疼的,还有如何维护一个单词的数据库也是需要仔细考虑的东西。想要背单词还是得靠真正下决心去坚持,不要老想着这种旁门左道,在Bash里混杂着这种功能也有些显得不伦不类。所以我最终在实现了通过文件维护单词本、通过随机数和一个开关控制单词输出频率之后的原型之后完全放弃了这个想法。

最后,关于PROMPT_COMMAND的用法,可以参考下面这两个链接:

我在用PROMPT_COMMAND的时候主要遇到了两个问题:

  • PROMPT_COMMAND里的命令是多行的话,需要在每一行末尾添加反斜线;
  • 如果PROMPT_COMMAND里面的命令需要使用双引号的话,那么需要将最外层的双引号改为单引号。

获取命令输出

可以用反引号将shell命令的输出赋给变量:testing=`date` 一个有意思的例子,通过反引号获得当前日期并用其生成唯一文件名:

#!/bin/bash 
# copy the /usr/bin directory listing to a log file
today=`date +%y%m%d` 
ls /usr/bin -al > log.$today

我们也可以用$(cmd)的方式来获取输出。

重定向输入和输出

  • 输出重定向。
    就是常用的>符号了。
    如果是追加数据的话,就是>>。
  • 输入重定向
    就是常用的<符号了。
    <<被称为内联输入重定向(inline input direction)。这种方式允许你在命令行先输入一个文本标记表示你要开始输入数据,然后输入具体的数据内容,最后输入同样的文本标记表示你已经把数据输入完毕了。如:
wc << EOF
test string 1 
test string 2 
test string 3 
EOF

管道

|就是管道,把前一个命令的输出和后一个命令的输入连起来。

执行数学运算

使用expr命令

a=$(expr 1 + 1)
echo $a

使用方括号

bash shell可以使用更简单的方式进行数学运算:$[ operation ]。即将数学运算式子放在方括号内,然后用$引用结果。
方括号里的*不会被解释为通配符。而方括号这种方式的主要限制在于只支持整数运算。

使用bc进行计算

  • 通过在bc中设置scale的值(小数点位数)来指定精度。
  • 通过echo命令和管道将要计算的式子发给bc,然后再用反引号获取计算结果。
  • 如果需要进行多行的计算,那么就要使用内联输入重定向了。
  • 只有通过bc或者类似工具才能进行浮点数的计算,bash/shell本身是无法进行浮点数计算的。
  #!/bin/bash

  var1=10.46
  var2=43.67
  var3=33.2
  var4=71

  var5=`bc << EOF
  scale = 4
  a1 = ( $var1 * $var2 )
  b1 = ( $var3 * $var4 )
  a1 + b1
  EOF
  `
  echo The final answer for this mess if $var5

退出脚本

shell中运行的每个命令都使用退出状态码(exit status)来告诉shell它完成了处理。退出状态码是一个0~255之间的整数值。

状态码参考:

状态码描述
0命令成功结束
1通用未知错误
2误用shell命令
126命令不可执行
127没找到命令
128无效退出参数
128+xLinux信号x的严重错误
130命令通过Ctrl+C终止
255退出状态码越界

可以通过$?专属变量来查看shell中执行的最后一条命令的退出状态码。默认情况下,shell脚本会以脚本中的最后一个命令的退出状态码退出,可以通过exit命令指定退出状态码,如果指定的退出状态码大于255,则取模。

控制流

使用if-then语句

if command
then
	commands
fi

需要注意的是,这里的if是通过命令的退出状态码是否为0来判断该命令是否成功执行,而不是像其他编程语言那样使用bool值。
在一些脚本中,可能会使用如下的格式:

if command; then
	commands
fi

if-then-else语句

if command
then
	commands
else
	commands
fi

嵌套if

if command
then
	commands
elif command2
then
	more commands
fi

elif可使用任意多个。

test命令

if-then语句不能测试跟退出状态码无关的条件,但是可以通过test命令完成这个任务。

test condition

可以直接使用test命令作判断:

if test condition
then
	commands
fi

或者通过使用方括号的方式来简化:

if [ condition ]
then
	commands
fi

必须用空格包裹condition,否则会报错
以及test命令不能处理浮点数
下面讲述test可以判断的三类条件:

数值比较

比较描述
n1 -eq n2检查n1是否与n2相等
n1 -ge n2检查n1是否大于或等于n2
n1 -gt n2检查n1是否大于n2
n1 -le n2检查n1是否小于或等于n2
n1 -lt n2检查n1是否小于n2
n1 -ne n2检查n1是否不等于n2

字符串比较

比较描述
str1 = str2检查str1是否和str2相同
str1 != str2检查str1是否和str2不同
str1 < str2检查str1是否比str2小
str1 > str2检查str1是否比str2大
-n str1检查str1的长度是否非0
-z str1检查str1的长度是否为0
  • 在进行大小比较时需要对大于、小于符号进行转义 可以这样理解,在if test condition中进行字符串大小比较,那么>或者<就会被解释成为重定向符号,因此需要进行转义。
  • test命令进行大小比较时,大写字母会被当成小于小写字母 而在sort命令中,小写字母会被当做小于大写字母。这是因为test命令按照ASCII数值进行排序,而sort命令使用的是系统的本地化语言设置中定义的排序顺序。对于英语,本地化设置指定了在排序顺序中小写字母出现在大写字母前。
  • 不懂为什么书上在进行字符串长度是否为0时为变量名(包括美元符)加上了引号,貌似这不是必要的,也许是为了强调可能出现包含空格的字符串这种情况吧。
#!/bin/bash
# testing string length
val1=testing
val2=''

if [ -n "$val1" ]
then
  echo "The string '$val1' is not empty"
else
  echo "The string '$val2' is empty"
fi

fi [ -z "$val2" ]
then
  echo "The string '$val2' is empty"
else
  echo "The string '$val2' is not empty"
fi

if [ -z "$val3" ]
then
  echo "The string '$val3' is empty"
else
  echo "The string '$val3' is not empty"
if

我们可以通过判断字符串长度为0的方式来判断一个变量是否存在/含有值。

文件比较

比较描述
-d file检查file是否存在并是一个目录
-e file检查file是否存在
-f file检查file是否存在并是一个文件
-r file检查file是否存在并可读
-s file检查file是否存在并非空
-w file检查file是否存在并可写
-x file检查file是否存在并可执行
-O file检查file是否存在并属当前用户所有
-G file检查file是否存在并且默认组与当前用户相同
file1 -nt file2检查file1是否比file2新
file1 -ot file2检查file1是否比file2旧

复合条件测试

if-then语句允许你使用布尔逻辑来组合测试。有两种布尔运算符可用:

  • [ condition1 ] && [ condition2 ]
  • [ condition1 ] || [ condition2 ]

if-then的高级特性

使用双圆括号处理数学表达式

符号描述
val++后增
val–后减
++val先增
–val先减
!逻辑求反
~位求反
**幂运算
<<左位移
>>右位移
&位布尔与
|位布尔或
&&逻辑与
||逻辑或

双圆括号里的表达式不需要进行转义操作
双圆括号不仅可以用在if语句中,也可以用在普通的变量赋值语句中。

使用方括号进行高级字符串处理功能

双括号里的表达式可以按照test命令的方式进行字符串比较;同时还提供了test命令未提供的另一个特性—模式匹配(pattern matching)
在模式匹配中,可以定义一个正则表达式来匹配字符串值。如:

#!/bin/bash
# using pattern matching

if [[ $USER == r* ]]
then
	echo "Hello $USER"
else
	echo "Sorry, I do not know you"
fi

case命令

如果不想写繁琐的if-then-else语句,可以使用case命令:

case variable in
pattern1 | pattern2) commands1;;
pattern3) commands2;;
*) default commands;;
esac

for命令

for var in list
do
	commands
done

或者换个风格:

for var in list: do

读取列表中的值

for test in Alabama Alaska Arizona Arkansas California Colorado
do
	echo The next state is $test
done

在循环结束之后,循环变量仍然可以被拿来使用,且会保持列表中的最后一个值。

如果列表中某个值有空白/单引号,那么需要为这个值加上双引号或者单引号,或者使用转义字符(反斜线);

用通配符读取目录

for file in /home/rich/test/*
	do
		if [ -d "$file" ]
		then
			echo "$file is a directory"
		elif [ -f "$file"]
		then
			echo "$file is a file"
		fi
	done
done

$file加上双引号是因为考虑到文件/目录名可能含有空格。

C语言风格的for命令

for (( variable assignment; condition; iteration process ))
do
	...
done

这种风格的for循环没有遵循bash shell的一些惯例:

  • 给变量赋值时可以有空格;
  • 条件(condition)中的变量不以美元符开头;
  • 迭代过程的算式没有用expr命令

使用多个变量

可以像下面这样在一个for循环中使用多个变量,但是只能定义一个条件:

for (( a=1, b=10; a<=10; a++,b-- ))
do
	...
done

while命令

while	test command1
		test command2
		...
		test commandn
do
	...
done

测试命令都会被执行,但是只有最后一个会被用来判断是否要结束循环。

until命令

until	test command1
		test command2
		...
		test commandn
do
	...
done

与while命令类似,也只有最后一个命令会被真正用来做判断。

break与continue命令

  • break 与C中break类似,但是多一个特殊的用法:break n 表示要跳出的循环次数。默认情况下(即我们没有指定n时),n为1。n每增大1,就多停止一层循环。
  • continue 与break类似,也有一个特殊的用法:continue n

shell脚本获取命令行参数

位置参数(positional parameter)

$0为程序名,$1到*9*依次为第一到第九个参数。如果需要第十个参数或者更多,可以用*9∗依次为第一到第九个参数。如果需要第十个参数或者更多,可以用∗{10}*,即从第十个参数开始需要在引用时为索引加上花括号。

$0的值会包括执行程序时输入的路径前缀(如果有输入这个路径前缀的话),可以通过basename命令去掉路径前缀。

特殊参数变量

  • $#表示命令行参数的个数(不包括程序名这个参数)。但是如果想用${$#}来表示最后一个参数是不可行的(大概在花括号内不能使用美元符?),需要使用${!#}
  • $*则把所有命令行参数当做一个字符串保存起来(不包括程序名);
  • $@会把所有命令行参数当做一个数组保存起来(不包括程序名);

shift命令

和batch中的shift类似,把参数往前移动,但是$0(即程序名)是不会变的,只有后面的参数在依次往前移。

解析命令行参数

最原始的方法

手动解析各个命令行参数,通过case语句执行相应的操作

getopt命令

getopt optstring options parameters

optstringCgetopt函数要求的格式一样。getopt命令的解析结果会把选项和参数一一对应起来,而对于多余的参数则会用双破折线分开。例子:

$ getopt ab:cd -a -b test1 -cd test2 test3
 -a -b test1 -c -d -- test1 test2

如果出现了未在optstring中的选项会报错,可以使用*-q*选项忽略这种错误。

在脚本中使用getopt命令

在脚本中可以通过如下语句用getopt命令格式化命令行参数(原始的命令行参数将被替换为格式之后的结果):

set -- `getopt -q ab:c "$@"`

getopts命令

getopt命令不能处理带空格的参数值,它会将空格作为参数分隔符。可以使用getopts命令避免这个问题。

getopts命令格式如下:

getopts optstring variable

例子:

#!/bin/bash
# simple demonstration of the getopts command

while getopts :ab:c opt
do
	case "$opt" in
	a)	echo "Found the -a option" ;;
	b)  echo "Found the -b option, with value $OPTARG";;
	c)	echo "Found the -c option" ;;
	*) echo "Unknown option: $opt" ;;
	esac
done

选项字符串前的冒号表示忽略错误消息。OPTARG变量储存了可能存在的与当前选项相对应的参数值,OPTIND变量存储了getopts正在处理的参数的位置。
getopts命令相对getopt命令的几个优点:

  • 可以在参数中包含空格
  • 可以将选项字母和参数值放在一起使用,而不用加空格。如-abtest1会被解析为-a -b test1
  • 会识别不存在的选项,并以问号形式进行提醒

通用的Linux命令选项

选项描述
-a显示所有对象
-c生成一个计数
-d指定一个目录
-e扩展一个对象
-f指定读入数据的文件
-h显示命令的帮助信息
-i忽略文本大小写
-l产生输出的长格式版本
-n使用非交互模式(批量)
-o指定将所有输出重定向到的输出文件
-q以安静模式运行
-r递归地处理目录和文件
-s以安静模式运行
-v生成详细输出
-x排除某个对象
-y对所有问题答yes

获得用户输入

可以使用read命令从标准输入(键盘)或者另一个文件描述符获取输入。read的具体用法可以通过bash的help命令查看。下面罗列了几个常用选项:

  • -p 指定提示符
  • -t 设置超时时间
  • -n 对输入字符计数
  • -s 用户的输入内容不会显示在屏幕上(比如输入密码时就需要这种功能)

read指定的变量数小于输入的单词数时,read会将多余的单词一起追加在最后一个变量里。

常用例子,逐行读取输入:

while read line
do
	echo $line
done

上面这个写法有一个陷阱,就是如果是读的文件的最后一行,并且该行没有newline,那么上面的脚本会少读一行。因为read在遇到EOF的时候是不会返回0的,可以看help read的说明。为了处理这种情况可以改成下面这种:

while read line || [ -n "$line" ]
do
	echo $line
done

因为虽然读到最后一行遇到了EOF,read没有返回0,但是内容还是读到了变量里面的。
然而上面的几行脚本看似简单,却还是容易出错,比如我当初自己按照这个思路写的是:

while read line || [ -n $line ]
do
	echo $line
done

就是这个变量line取值时没有加引号,导致上面的循环成为了一个死循环。原因在于当read读到的内容是空的时候,line的值为空,上面的[ -n $line ]判断就变成了了[ -n ],这个判断的结果是true。为什么这个判断结果是true呢,可以试一下这几句命令:

if [ ]; then echo yes; fi
if [ -gt ]; then echo yes; fi
if [ -lt ]; then echo yes; fi
if [ -n ]; then echo yes; fi
if [ > ]; then echo yes; fi

可以发现第一个判断结果是false,中间3个判断结果是true,最后一个会报语法错误。要解释这一切,先查看test的说明,help test,里面有一段:

See the bash manual page bash(1) for the handling of parameters (i.e. missing parameters).

好,接着看man bash

说的是如果test命令(也就是[ ])里只有一个操作数是直接返回true的。而[ > ]特殊在于>是一个用于输出重定向的特殊符合,因此这里直接报了语法错误。
Stackoverflow上有一个回答也讲解了这个问题。如其中一个评论所说,[ STRING ]其实就是用来判断里面的STRING是否为空的,[ -lt ][ -n ]里面的运算符此时由于操作数的缺失,都直接按照[ STRING ]解释为字符串了。
Hackerrank上面考这个知识点的问题

创建函数

两种方式:

  • function name { commands }
  • name() { commands }

使用函数

  • 可以像使用普通命令那样使用函数
  • 需要在使用函数前定义好函数
  • 向函数传递参数与向脚本传递参数一样

将函数作为库使用

source lib.sh
或者
. lib.sh

.是source的快捷别名。

全局变量/局部变量

  • 函数内可以使用全局变量
  • 在函数内声明/定义局部变量:local temp 或者 local temp=`hello world`

shell中的可交互图形组件

假设想让shell脚本和可执行程序一样可以和用户进行交互,那么可以考虑:

使用原始的read命令和case语句来让用户输入指定的选项

使用select命令来简化这一过程

select variable in list
do
	commands
done

使用dialog

dialog --widget parameters

dialog命令会将用户数据发送到STDERR

更高级的dialog

KDE下有kdialog,GNOME下有gdialogzenity

文件描述符重定向

Linux的标准文件描述符

文件描述符缩写描述
0STDIN标准输入
1STDOUT标准输出
2STDERR标准输出

简单的重定向

  • 重定向输入 使用<可以从文件中读取输入
  • 重定向输出 使用>可以将输出写如到指定文件;>>则表示追加数据到指定文件
  • 重定向错误
    • 2 > filename表示将错误信息写到名为filename的文件中;
    • 1>file1 2>file2表示将标准输出重定向到file1,将错误重定向到file2
    • 如果想将标准输出和错误重定向到一个文件,可以使用&>的简写方式;

在脚本中重定向

临时重定向

通过>&2(为区分文件名和文件描述符,所以在文件描述符前加了个&)将单个命令的输出重定向到STDERR;

永久重定向

  • exec 1>filename将STDOUT重定向到filename这个文件中
  • 同理exec 2>filename将STDERR重定向到filename这个文件中
  • exec 0< filename重定向标准输入为filename这个文件

创建自己的文件描述符

shell中最多有9个打开的文件描述符,其他6个文件描述符从3排到8。

  • 创建输出文件描述符 exec 3>filename

重定向文件描述符

exec 3>&1;可以通过先将一个文件描述符重定向到标准输出,然后再把标准输出重定向到指定文件,再将标准输出重定向到之前的文件描述符的方式来实现一段时间的重定向标准输出到指定文件,如下所示:

exec 3>&1
exec 1>testout

code...

exec 1>&3

创建读写文件描述符

exec 3<> testfile。这个读写文件描述符和Unix/Linux里的读写文件描述符一致,只不过是文本模式的。

关闭文件描述符

exec 3>&-

列出打开的文件描述符

lsof

阻止命令输出

> /dev/null

创建临时文件

  • 本地临时文件 mktemp filename.XXXXXX
  • 在/tmp目录下创建临时文件 mktemp -t filename.XXXXXX
  • 创建临时目录 mktemp -d dirname.XXXXXX

同时输出和重定向

如果想要一边看命令的输出一边将这个输出保存到文件里面而不是手动拷贝屏幕上的输出到文件里,可以考虑使用tee命令。

command <args...>  | tee <path-to-output-file>

tee命令捕获的是stdout的内容,如果command命令输出的某些东西是通过stderr输出的呢?
首先更准确的说法应该是bash的管道操作符|只重定向(这个”重定向”是指通过管道连接) 前面命令的stdout到tee命令的输入。那么如果想要tee命令捕获前一个命令的stderr输出 ,只要让前一个命令的stderr重定向(这个”重定向”是指文件描述符的重定向)到该命令的 stdout就好了。复习一下bash中的文件描述符重定向,让前一个命令的stderr输出到 stdout:

#仅对单个命令生效的重定向方式
<command> 2>&1 <path-to-output-file>
#|&是2>&1的一种简写方式,https://unix.stackexchange.com/a/24340/191816
<command> |& tee <path-to-output-file>
#下面这条命令这行后,后面的命令的stderr都重定向到stdout了
exec 2>&1

使用Bash收发网络数据包

使用pseudo device,shell可以直接收发UDP/TCP包:

exec 5<>/dev/tcp/www.baidu.com/80
echo -e "GET / HTTP/1.0\n" >&t
cat <&5

/dev/tcp/dev/udp可以分别用来收发TCP协议和UDP协议的数据包。

环境变量

bash shell用一个称作环境变量(enviroment variables)的特性来存储有关shell会话和工作环境的信息。

  • 全局变量 不仅对shell会话可见,对所有shell创建的子进程也可见。
  • 局部变量 只对创建它们的shell可见,子进程不可见。

查看全局变量:

printenv

显示单个环境变量:

echo $VAR

在Linux系统中并没有一个命令用于查看局部环境变量,但是可以用set命令显示为某个特定进程设置的所有环境变量(包括了全局环境变量)。

设置环境变量

  • 设置局部环境变量 用等号赋值即可:test=testing 如果值里有空格需要用单引号包裹,否则shell会被空格后的部分当做命令执行。
  • 设置全局环境变量 先创建一个局部环境变量,然后再把它导出到全局环境中。
    当然也可以直接用export VAR=VAL的方式test=testing echo $test export test 在使用export时记得不要加上美元符。

删除环境变量

使用unset命令可以删除环境变量,同样记得不要加上美元符号。

unset test

在删除全局环境变量时,如果是在子进程中删除了一个全局环境变量,那么这个操作只对子进程有效,即该全局环境变量在父进程中依然有效。

默认shell环境变量

  • bash shell支持的Bourne变量列表:
  • bash shell环境变量

定位系统环境变量

启动bash shell有3种方式:

  • 登录时当做默认登录shell
  • 作为非登录shell的交互式shell
  • 作为运行脚本的非交互式shell

登录shell

当你登录Linux系统时,bash shell会作为登录shell启动。登录shell会从4个不同的启动文件里读取命令。下面是bash shell处理这些文件的次序:

  • /etc/profile
  • $HOME/.bash_profile
  • $HOME/.bash_login
  • $HOME/.profile

/etc/profile文件是系统上默认的bash shell的主启动文件。系统上的每个用户登录时都会执行这个启动文件。另外3个文件是用户专有的。 剩下的3个启动文件都起着同一个作用:提供一个用户专属的启动文件来定义用户专有的环境变量。大多数Linux发行版只用这3个启动文件中的一个。

比如我的Ubuntu系统就只有HOME/.profile,其内容就是尝试读取HOME/.profile,其内容就是尝试读取HOME/.bashrc,然后设置PATH。

交互式shell

如果你的bash shell不是登录时启动的(比如你在命令行提示符下敲入bash启动),你启动的shell称作交互式shell。交互式shell不会像登录shell一样运行,但它依然提供了命令行提示符来输入命令。
如果bash是作为交互式shell启动的,它不会去访问/etc/profile文件,而会去用户的HOME目录检查.bashrc是否存在。

以我的Ubuntu系统为例,在$HOME/.bashrc中主要设置了一些命令的别名和终端属性。
交互式shell的启动文件只会在每次有新的交互式shell启动时才运行,因此任何子shell都会自动执行这个交互式shell的启动文件。

非交互式shell

系统执行shell脚本时会使用这种shell,bash shell提供了BASH_ENV环境变量。当shell启动一个非交互式shell进程时,它会检查这个环境变量来查看要执行的启动文件。如果有指定的,shell会执行文件里的命令。

取消变量

可以用unset命令来取消整个数组或者数组中的某个值,但是在取消某个值的时候,unset命令只会将该值对应的位置的值设置为空,因此不会改变数组里其他值的位置索引。

使用命令别名

创建命令别名的方式类似创建环境变量:

alias li='ls -il`

查看已有的命令别名:

alias -p

处理信号

Linux信号

信号描述
1SIGHUP挂起进程
2SIGINT终止进程
3SIGQUIT停止进程
9SIGKILL无条件终止进程
15SIGTERM可能的话终止进程
17SIGSTOP无条件停止进程,但不是终止进程
18SIGTSTP停止或暂停进程,但不终止进程
19SIGCONT继续运行停止的进程

默认情况下,bash shell会忽略收到的任何SIGQUIT和SIGTERM信号。如果bash shell收到了SIGHUP信号,它会退出。但在退出之前,它会将SIGHUP信号传给shell启动的所有进程(比如shell脚本)。通过SIGINT信号可以中断shell。同时shell会将SIGINT信号传给shell启动的所有进程。

产生信号

终止进程

Ctrl+C:SIGINT

暂停进程

Ctrl+Z:SIGTSTP

$ sleep 1000
^Z
[1]+	Stopped				sleep 1000
$

方括号中的数字为作业号。 查看已停止的作业:

ps au

在STAT一列状态为T的表示该命令正在被跟踪或者被停止了。

捕捉信号

trap commands signals

commands需要用引号包裹以和signals区分开。

移除捕捉

trap - signals

作业控制

以后台模式运行脚本

在命令后加上&即可。
如果退出shell,启动的后台脚本/程序也会退出

在非控制台下运行脚本

nohup prog &

这种方法即使shell退出,shell中启动的进程也不会退出。使用nohup后,输出会出现在nohup.out文件中。

查看作业

通过jobs命令可以查看当前shell正在处理的作业。jobs命令参数:

参数 | 描述 -l | 列出进程的PID以及作业号 -n | 只列出上次shell发出的通知后改变了状态的作业 -p | 只列出作业的PID -r | 只列出运行中的作业 -s | 只列出已停止的作业

jobs命令输出中的加号代表默认作业,在使用作业控制命令时,如果未指定作业号那么该作业会被当做操作对象。带减号的作业则会在当前默认作业完成处理时成为下一个默认作业。任何时候都只有一个带加号的作业和一个带减号的作业,不管shell中有多少各正在运行的作业。

重启停止的作业

bg JOB_NUM
fg JOB_NUM

谦让度

nice值的范围为-20到19,越nice优先级越低。

查看当前shell进程的nice值

nice

启动进程时指定nice值

nice -n prog

nice命令阻止普通系统用户增加命令的优先级。

调整nice值

renice NICE_VAL -p PID 

定时运行作业

at [-f filename] time

关于时间的格式:

  • 标准的小时和分钟格式,比如10:15;
  • ~AM/~PM指示符,比如10:15~PM;
  • 特定可命名的时间,比如now、noon、midnight或者teatime(4~PM)。如果指定了一个已经过去的时间,at命令会在第二天该时间运行该作业。
  • 标准日期格式,比如MMDDYY、MM/DD/YY或DD.MM.YY;
  • 文本日期,比如Jul 4或Dec 25,加不加年份均可。
  • 当前时间+25min;
  • 明天10:15~PM;
  • 10:15+7天

使用at命令时,该作业会被提交到作业队列(job queue)中。作业队列会保存通过at命令提交的待等待处理的作业。针对不同优先级,存在不同的作业队列。作业队列通常用小写字母a~z来引用。作业队列的字母排序越高,作业运行的优先级就越低。默认情况下,at的作业会被提交到at作业队列。如果你想以更高的优先级运行作业,你可以用-q参数指定不同的队列字母。

batch命令可以看作是at的alias,具体用法查看manual。

获取作业的输出

作业在运行时并没有关联任何屏幕,Linux系统会将用户的E-mail地址作为STDOUT和STDERR。

在大多数Linux发行版中,赋给/bin/sh的默认shell是bash shell。但Ubuntu将dash shell作为其默认shell。

列出等待的作业

atq

删除作业

atrm JOB_NUM

计划定期执行脚本

cron时间表格式:

min hour dayofmonth month dayofweek command

不关心的数值可以用通配符(星号)。

如何设置一个命令在每个月的最后一天执行:00 12 * * * if [date +%d -d tomorrow= 01 ] ; then : command

构建时间表

crontab -e

查看crontab内容

crontab -l

Bash中的数组和map

最近才清醒认识到Bash 4是支持map的,以前还一直以为Bash只能做没有技术含量的活,想实现类似map的功能需要自己去写复杂无比的函数。

Bash中Map的基本操作

声明(定义)

declare -A mymap

-A表示定义一个关联数组,也就是mymap了。
也可以在定义时就赋值一些key和val:

declare -A mymap=( [key1]=val1 [key2]=val2 [key3]=val3 )

赋值

mymap[key]=val

根据key查找val

echo ${mymap[key]}

删除key

unset mymap[key]

删除后或者map中本来就没有这个key,那么去取map中这个key对应的val,获得的值是空值。和c++不一样,c++只要用[]操作符去访问了一个key就会自动插入这个key,而Bash中的map必须要有显示的赋值操作才会插入这个key。

测试是否包含某个key

对于map中本来就不存在的key,你取查找对应的val,获得的结果是空值,那么怎么区分这个key对应的val是一个空值还是map中本来就不存在这个key呢?

if [ ${mymap[key]+_} ]
then
  echo found
fi

删除整个map

unset mymap

如果只是想把map的内容清空,那就先unset,再重新声明一个名字一样的map好了。

获取key的集合

echo ${!mymap[@]}

获取val的集合

echo ${mymap[@]}

获取key的个数

echo ${#mymap[@]}

Bash中Array的基本操作

理清了map的用法,顺便也理一下bash中数组的用法吧。

声明(定义)

declare -a myarr
declare -a myarr=(a b c)
myarr=(a b c)

通过myarr[index]=val的方式隐式声明的貌似是一个map而不是array。以及数组貌似是可以直接转换为map的,key就是索引,当你用访问map的方式去访问数组时,就自动给你转成map了。

获取数组长度

这个操作测试我测试的时候发现其实是变成跟map一样的语义了,是这个map里元素的个数,而不是数组长度这种东西,也就是说如果你这个数组只有索引为4的元素,那么这个数组长度就是1,而不是说还给你保存了索引为0,1,2,3的元素的位置的。

echo ${#myarr[@]}

其他操作

参考map的操作,基本都是一样的。

readarray函数

通过名字引用变量

如何传递一个map或者数组给Bash编写的函数呢?我目前发现的唯一好方法就是通过变量名的引用。

declare -A mymap=( [key1]=val1 [key2]=val2 )
function printMap () {
  local -n localMap=$1
  echo ${localMap[@]}
}
printMap mymap

如上面代码片段所示,这次我们给函数传递的是变量名,而不是像平常那样前面加上$传递变量的值,这样函数内部就可以通过local -n(也可以根据自己的需求使用declare -n)声明一个引用,后续就可以通过这个引用访问要传递的map的内容了。

对数组使用filter

把含有字母a的元素给替换成空:

echo  ${Array[@]/*[aA]*/}

Bash中的字符串

字符串也算是一种数组吧,那么同样的总结一下字符串的用法吧。

生成指定长度的只包含某个字符的字符串

printf '=%.0s' {1..100}

修改字符串中指定位置的值

就是两种思路,要么用sed找到第x个字符进行替换,要么用bash中为string提供的子字符串功能进行拼接:

echo $theStr | sed s/./A/5
a="............"
b="${a:0:4}A${a:5}"
echo ${b}

获取变量类型

Advanced Bash Programming

最后推荐一本书,Advanced Bash Programming,虽然有点老,但是讲的比较全,写Bash脚本时不知道怎么写可以翻下看有没有可用的例子。

任务管理

kill by job id

kill %job_id