shell相关4

j脚本除错

如何考虑命令出错的情况。
比如:

#! /bin/bash

dir_name=/path/not/exist

cd $dir_name
rm *

上面脚本中,如果目录$dir_name不存在,cd $dir_name命令就会执行失败。如果$dir_name为空,则会进入主目录删除所有文件。
以下写法才是正确:

[[ -d $dir_name ]] && cd $dir_name && rm *

如果不放心删除什么文件,可以先打印出来看一下:

[[ -d $dir_name ]] && cd $dir_name && echo rm *

上面命令中,echo rm *不会删除文件,只会打印出来要删除的文件。

bash的-x参数可以在执行每一行命令之前,打印该命令:
比如:

# script.sh
echo hello world

加上参数-x之后的执行结果如下:

$ bash -x script.sh
+ echo hello world
hello world

也可以将-x写在Shebang行中:

#! /bin/bash -x
# trouble: script to demonstrate common errors

number=1
if [ $number = 1 ]; then
  echo "Number is equal to 1."
else
  echo "Number is not equal to 1."
fi

执行结果如下:

$ trouble
+ number=1
+ '[' 1 = 1 ']'
+ echo 'Number is equal to 1.'
Number is equal to 1.

输出的命令之前的+号,是由系统变量PS4决定,可以修改这个变量:

$ export PS4='$LINENO + '
$ trouble
5 + number=1
7 + '[' 1 = 1 ']'
8 + echo 'Number is equal to 1.'
Number is equal to 1.

环境变量:
变量$LINENO返回它在脚本里面的行号:

#!/bin/bash

echo "This is line $LINENO"

运行结果如下:

$ ./test.sh
This is line 3

变量$FUNCNAME返回一个数组,内容是当前的函数调用堆栈。该数组的0号成员是当前调用的函数,1号成员是调用当前函数的函数,以此类推:

function func1()
{
  echo "func1: FUNCNAME0 is ${FUNCNAME[0]}"
  echo "func1: FUNCNAME1 is ${FUNCNAME[1]}"
  echo "func1: FUNCNAME2 is ${FUNCNAME[2]}"
  func2
}

function func2()
{
  echo "func2: FUNCNAME0 is ${FUNCNAME[0]}"
  echo "func2: FUNCNAME1 is ${FUNCNAME[1]}"
  echo "func2: FUNCNAME2 is ${FUNCNAME[2]}"
}

func1

运行结果如下:

$ ./test.sh
func1: FUNCNAME0 is func1
func1: FUNCNAME1 is main
func1: FUNCNAME2 is
func2: FUNCNAME0 is func2
func2: FUNCNAME1 is func1
func2: FUNCNAME2 is main

可以发现,有一个main函数。

变量$BASH_SOURCE返回一个数组,内容是当前的脚本调用堆栈。该数组的0号成员是当前执行的脚本,1号成员是调用当前脚本的脚本,以此类推,跟变量$FUNCNAME是一一对应关系。
下面有两个子脚本lib1.sh和lib2.sh:

# lib1.sh
function func1()
{
  echo "func1: BASH_SOURCE0 is ${BASH_SOURCE[0]}"
  echo "func1: BASH_SOURCE1 is ${BASH_SOURCE[1]}"
  echo "func1: BASH_SOURCE2 is ${BASH_SOURCE[2]}"
  func2
}
# lib2.sh
function func2()
{
  echo "func2: BASH_SOURCE0 is ${BASH_SOURCE[0]}"
  echo "func2: BASH_SOURCE1 is ${BASH_SOURCE[1]}"
  echo "func2: BASH_SOURCE2 is ${BASH_SOURCE[2]}"
}

然后,主脚本main.sh调用上面两个子脚本:

#!/bin/bash
# main.sh

source lib1.sh
source lib2.sh

func1

执行主脚本main.sh,会得到下面的结果:

$ ./main.sh
func1: BASH_SOURCE0 is lib1.sh
func1: BASH_SOURCE1 is ./main.sh
func1: BASH_SOURCE2 is
func2: BASH_SOURCE0 is lib2.sh
func2: BASH_SOURCE1 is lib1.sh
func2: BASH_SOURCE2 is ./main.sh

变量$BASH_LINENO返回一个数组,内容是每一轮调用对应的行号。${BASH_LINENO[$i]}${FUNCNAME[$i]}是一一对应关系,表示${FUNCNAME[$i]}在调用它的脚本文件${BASH_SOURCE[$i+1]}里面的行号。
下面有两个子脚本lib1.sh和lib2.sh:

# lib1.sh
function func1()
{
  echo "func1: BASH_LINENO is ${BASH_LINENO[0]}"
  echo "func1: FUNCNAME is ${FUNCNAME[0]}"
  echo "func1: BASH_SOURCE is ${BASH_SOURCE[1]}"

  func2
}
# lib2.sh
function func2()
{
  echo "func2: BASH_LINENO is ${BASH_LINENO[0]}"
  echo "func2: FUNCNAME is ${FUNCNAME[0]}"
  echo "func2: BASH_SOURCE is ${BASH_SOURCE[1]}"
}

然后,主脚本main.sh调用上面两个子脚本:

#!/bin/bash
# main.sh

source lib1.sh
source lib2.sh

func1

执行主脚本main.sh,会得到下面的结果:

$ ./main.sh
func1: BASH_LINENO is 7
func1: FUNCNAME is func1
func1: BASH_SOURCE is main.sh
func2: BASH_LINENO is 8
func2: FUNCNAME is func2
func2: BASH_SOURCE is lib1.sh

上面例子中,函数func1是在main.sh的第7行调用,函数func2是在lib1.sh的第8行调用的。

mktemp 命令,trap 命令

Bash 脚本有时需要创建临时文件或临时目录。常见的做法是,在/tmp目录里面创建文件或目录,这样做有很多弊端,使用mktemp命令是最安全的做法。
/tmp目录是所有人可读写的,任何用户都可以往该目录里面写文件。创建的临时文件也是所有人可读的:

$ touch /tmp/info.txt
$ ls -l /tmp/info.txt
-rw-r--r-- 1 ruanyf ruanyf 0 12月 28 17:12 /tmp/info.txt

因此,临时文件最好使用不可预测、每次都不一样的文件名,防止被利用。临时文件使用完毕,应该删除。但是,脚本意外退出时,往往会忽略清理临时文件。
生成临时文件应该遵循下面的规则:

创建前检查文件是否已经存在。
确保临时文件已成功创建。
临时文件必须有权限的限制。
临时文件要使用不可预测的文件名。
脚本退出时,要删除临时文件(使用trap命令)。

mktemp命令就是为安全创建临时文件而设计的。虽然在创建临时文件之前,它不会检查临时文件是否存在,但是它支持唯一文件名和清除机制,因此可以减轻安全攻击的风险。
直接运行mktemp命令,就能生成一个临时文件:

$ mktemp
/tmp/tmp.4GcsWSG4vj

$ ls -l /tmp/tmp.4GcsWSG4vj
-rw------- 1 ruanyf ruanyf 0 12月 28 12:49 /tmp/tmp.4GcsWSG4vj

Bash 脚本使用mktemp命令的用法如下:

#!/bin/bash

TMPFILE=$(mktemp)
echo "Our temp file is $TMPFILE"

为了确保临时文件创建成功,mktemp命令后面最好使用 OR 运算符||,保证创建失败时退出脚本:

#!/bin/bash

TMPFILE=$(mktemp) || exit 1
echo "Our temp file is $TMPFILE"

为了保证脚本退出时临时文件被删除,可以使用trap命令指定退出时的清除操作:

#!/bin/bash

trap 'rm -f "$TMPFILE"' EXIT

TMPFILE=$(mktemp) || exit 1
echo "Our temp file is $TMPFILE"

mktemp 命令的参数:
-d:可以创建一个临时目录:

$ mktemp -d
/tmp/tmp.Wcau5UjmN6

-p:可以指定临时文件所在的目录。默认是使用$TMPDIR环境变量指定的目录,如果这个变量没设置,那么使用/tmp目录:

$ mktemp -p /home/ruanyf/
/home/ruanyf/tmp.FOKEtvs2H3

-t:可以指定临时文件的文件名模板,模板的末尾必须至少包含三个连续的X字符,表示随机字符,建议至少使用六个X。默认的文件名模板是tmp.后接十个随机字符:

$ mktemp -t mytemp.XXXXXXX
/tmp/mytemp.yZ1HgZV

trap命令:
trap命令用来在 Bash 脚本中响应系统信号。
最常见的系统信号就是 SIGINT(中断),即按 Ctrl + C 所产生的信号。trap命令的-l参数,可以列出所有的系统信号:

$ trap -l

trap的命令格式如下:

$ trap [动作] [信号1] [信号2] ...

上面代码中,“动作”是一个 Bash 命令,“信号”常用的有以下几个:

HUP:编号1,脚本与所在的终端脱离联系。
INT:编号2,用户按下 Ctrl + C,意图让脚本终止运行。
QUIT:编号3,用户按下 Ctrl + 斜杠,意图退出脚本。
KILL:编号9,该信号用于杀死进程。
TERM:编号15,这是kill命令发出的默认信号。
EXIT:编号0,这不是系统信号,而是 Bash 脚本特有的信号,不管什么情况,只要退出脚本就会产生。

trap命令响应EXIT信号的写法如下:

$ trap 'rm -f "$TMPFILE"' EXIT

上面命令中,脚本遇到EXIT信号时,就会执行rm -f "$TMPFILE"

trap 命令的常见使用场景,就是在 Bash 脚本中指定退出时执行的清理命令:

#!/bin/bash

trap 'rm -f "$TMPFILE"' EXIT

TMPFILE=$(mktemp) || exit 1
ls /etc > $TMPFILE
if grep -qi "kernel" $TMPFILE; then
  echo 'find'
fi

上面代码中,不管是脚本正常执行结束,还是用户按 Ctrl + C 终止,都会产生EXIT信号,从而触发删除临时文件。
注意,trap命令必须放在脚本的开头。否则,它上方的任何命令导致脚本退出,都不会被它捕获。

如果trap需要触发多条命令,可以封装一个 Bash 函数:

function egress {
  command1
  command2
  command3
}

trap egress EXIT

登录session&非登录session

登录session:
登录 Session 是用户登录系统以后,系统为用户开启的原始 Session,通常需要用户输入用户名和密码进行登录。
登录 Session 一般进行整个系统环境的初始化,启动的初始化脚本依次如下:

/etc/profile:所有用户的全局配置脚本。
/etc/profile.d目录里面所有.sh文件
~/.bash_profile:用户的个人配置脚本。如果该脚本存在,则执行完就不再往下执行。
~/.bash_login:如果~/.bash_profile没找到,则尝试执行这个脚本(C shell 的初始化脚本)。如果该脚本存在,则执行完就不再往下执行。
~/.profile:如果~/.bash_profile和~/.bash_login都没找到,则尝试读取这个脚本(Bourne shell 和 Korn shell 的初始化脚本)。

下面是一个典型的.bash_profile文件:

# .bash_profile
PATH=/sbin:/usr/sbin:/bin:/usr/bin:/usr/local/bin
PATH=$PATH:$HOME/bin

SHELL=/bin/bash
MANPATH=/usr/man:/usr/X11/man
EDITOR=/usr/bin/vi
PS1='\h:\w\$ '
PS2='> '

if [ -f ~/.bashrc ]; then
. ~/.bashrc
fi

export PATH
export EDITOR

非登录session:
非登录 Session 是用户进入系统以后,手动新建的 Session,这时不会进行环境初始化。比如,在命令行执行bash命令,就会新建一个非登录 Session。
非登录 Session 的初始化脚本依次如下:

/etc/bash.bashrc:对全体用户有效。
~/.bashrc:仅对当前用户有效。

注意,执行脚本相当于新建一个非互动的 Bash 环境,但是这种情况不会调用~/.bashrc

~/.bash_logout脚本在每次退出 Session 时执行,通常用来做一些清理工作和记录工作,比如删除临时文件,记录用户在本次 Session 花费的时间。

为了方便 Debug,有时在启动 Bash 的时候,可以加上启动参数:

-n:不运行脚本,只检查是否有语法错误。
-v:输出每一行语句运行结果前,会先输出该行语句。
-x:每一个命令处理之前,先输出该命令,再执行该命令。
$ bash -n scriptname
$ bash -v scriptname
$ bash -x scriptname

命令提示符

用户进入 Bash 以后,Bash 会显示一个命令提示符,用来提示用户在该位置后面输入命令。
命令提示符通常是美元符号$,对于根用户则是井号#。这个符号是环境变量PS1决定的,执行下面的命令,可以看到当前命令提示符的定义:

$ echo $PS1

Bash 允许用户自定义命令提示符,只要改写这个变量即可。改写后的PS1,可以放在用户的 Bash 配置文件.bashrc里面,以后新建 Bash 对话时,新的提示符就会生效。要在当前窗口看到修改后的提示符,可以执行下面的命令:

$ source ~/.bashrc

除了PS1,Bash 还提供了提示符相关的另外三个环境变量。
环境变量PS2是命令行折行输入时系统的提示符,默认为>

$ echo "hello
> world"

环境变量PS3是使用select命令时,系统输入菜单的提示符:
环境变量PS4默认为+。它是使用 Bash 的-x参数执行脚本时,每一行命令在执行前都会先打印出来,并且在行首出现的那个提示符。
比如下面是脚本test.sh:

#!/bin/bash

echo "hello world"

使用-x参数执行这个脚本:

$ bash -x test.sh
+ echo 'hello world'
hello world

参考:https://wangdoc.com/bash/prompt.html

©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容