1.变量 variable
变量名是其所指向值得一个占位符(placeholder),引用变量值的过程我们称之为变量替换(variable substitution),变量名仅在赋值(=、+=、while read var1 var2、for var in 1 2 3)、定义环境变量(export)、算术运算中使用双括号结构($((var1-var2)))、数组下标中括号中(${array[var1]})和代表信号的时候(kill -l或者trap -l查询信号signal列表)不用加【$】前缀,其他情况下需要加前缀,需要注意的是在$符号并不在所有情况下都能翻译变量值,在单引号''强引用的情况下只是变量只是字符。
1.1 变量的赋值与引用
变量的赋值规则比较简单,上面的篇幅中给了四种赋值方式,【=】赋值的时候两端不能出现空格,出现空格会有不同的含义表达
variable= value ## 脚本会将value作为命令执行同时设置环境变量variable为空 variable =value ## 脚本会将variable当作命令,=value作为variable命令的执行参数
【+=】是追加变量内容
var+=a var+="b" var+=" c" var+=" d" echo $var \| ${var[0]} \| "$var" \| x${var[1]} > ab c d | ab c d | ab c d | x ## 从上述的引用值得显示可以发现所有的变量都是只有一个元素的数组,在""引用的时候,会保留变量的空白符
【while read var1 var2 var3】和【for var in value1 value2 value3】
echo "a b c" | while read x y z;do echo $z $y $x;done > c b a cat filename.txt > a 1 > b 2 while read char num;do echo $num $char;done < filename.txt > 1 a > 2 b ##以上是两种管道形式,结合while read给变量循环赋值 for i in "1" "2" "3";do echo $i;done > 1 > 2 > 3 for i in 1 2 3;do echo $i;done > 1 > 2 > 3 for i in "1 2 3";do echo $i;done > 1 2 3 var="1 2 3";for i in $var;do echo $i;done > 1 > 2 > 3 for i in "$var";do echo $i;done > 1 2 3
【'command'】
var=`find /var/log/ -name "*.log"`;echo "$var" > /var/log/tuned/tuned.log > /var/log/audit/audit.log > /var/log/anaconda/anaconda.log > .......
空变量在算术计算的时候默认为0
var= ##只声明var变量,不赋予任何初始值 if [ -z "$var" ];then echo "\$var is null";else echo "\$var exist";fi > $var is null unset var ##删除变量 if [ -z "$var" ];then echo "\$var is null";else echo "\$var exist";fi > $var is null ##从上述的执行结果来看,未定义变量和空变量一样,不定义也认为是存在的,是空值(null),在算术计算的时候为0 let var+=1;echo $var > 1
1.2 Bash变量的特性
Bash变量是弱类型的,即Bash的变量没有数值、字符串和日期等属性,本质上讲Bash变量只是字符串,但是在某些情况下如果变量的值是特定数值的时候,也允许做算术运算和比较。
a=2334;let a+=1;echo $a > 2335 a=23.3;let a+=1;echo $a > -bash: let: 23.3: syntax error: invalid arithmetic operator (error token is ".3")
在做算术运算的时候,小数的运算会因为小数点和加减号一起使用报错,提起变量属性可能有人就会想起declare这个命令,可以用于变量的属性声明,但是我们发现其实并没有太大作用,字符串变量并不会因为declare -i而变成整数。
这里需要注意的是Bash变量的弱变量属性,使得脚本并不会有持续跟踪变量类型负担,我们也应该不主动使用声明来增加该负担。
1.3 特殊的变量类型
1.3.1 局部变量(local)和全局变量(环境变量/environmental)
每当我们启动shell的时候,都或获取到很多的环境变量,在这里需要了解一下父子shell(father process and child process)的概念,我们连接到服务器启动shell的时候,就开启了一个父shell,这个父shell会获得系统默认的和在/etc/profile、/etc/profile.d/*和~/.bashrc中设置的变量作为环境变量,这时候我们再执行bash命令就会开启一个子shell,这个子shell会继承父shell的全部环境变量,但是我们在子shell中export var=value设置环境变量之后,再退出子shell会发现父shell中并不存在var变量,执行脚本的过程就是开启一个子shell的过程。
分配给环境变量的空间有限的,创建过多的环境变量或占用空间过大的环境变量可能会造成问题。
1.3.2 位置参数变量(Positional parameters)
vim position.sh > #!/bin/bash > echo $# > echo $* > echo $@ > for i in `seq 0 1 11`;do echo $i;done ./position.sh a b c d e f g h i j k > 11 ## $#代表位置参数的数量 > a b c d e f g h i j k > a b c d e f g h i j k ## $*和$@代表全部的位置参数,暂时接触不多 > ./position.sh ## $0代表命令本身 > a ## $1代表紧跟脚本的第一个参数 ..... > i > a1 > a2 ##当参数超过9之后,echo $10的结果是echo ${1}0,所以在参数超过9之后必须用 echo ${10}
还有一个位置参数移动命令 shift命令,每用一次就会去掉$1,后续所有的位置参数位次降1。
vim shift_test.sh ##利用循环加shift遍历位置变量和for i in ${@}效果一致 #!/bin/bash until [ -z $1 ] do echo $1 shift done ./shift_test.sh a 1 x > a > 1 > x
1.3.3 分隔符变量IFS
IFS变量是我认为较为常用的变量,它是系统默认分隔符变量,如果不做设置,就是空格或者tab,我喜欢用while read line的方式来读入文件内容,如果文件元素比较简单,有规律性,用IFS来分隔文件内容非常的方便,比如csv文件中用逗号作为元素分隔符号,这样就可以设置IFS为【,】号,有时候可能我们要把文件的每一行当作变量,这时候IFS就要用行结尾符号【$'\n'】作为值了。
vim example.txt > a b > 1 2 for i in `cat example.txt`;do echo $i;done > a > b > 1 > 2 old_IFS=$IFS;export IFS=$'\n';for i in `cat example.txt`;do echo $i;done;IFS=$old_IFS > a b > 1 2
2.换个角度看变量
在脚本中恰当的使用变量可以增强脚本并提高适应性,因此就需要了解变量的微妙之处和细微差异。
2.1 内建变量
Bash的内建变量种类非常的丰富,内建变量是影响Bash脚本行为的变量。、
$BASH ##Bash程序的路径 $BASH_ENV ##指向一个Bash启动文件,该文件在脚本被调用时会被读取 $BASH_SUBSHELL ##提示subshell所在层级 (echo $BASH_SUBSHELL;(echo $BASH_SUBSHELL)) > 1 > 2 $BASHPID ##当前bash的进程的pid号,结果通常和$$一致 $BASH_VERSINFO ##这是一个6元素数组 echo ${BASH_VERSINFO[*]} > 4 4 19 1 release x86_64-redhat-linux-gnu > 主版本号 次版本号 补丁号 构建版本号 发行状态 架构(与后面的$MACHTYPE相同) $BASH_VERSION ##Bash的版本信息 $CDPATH ##这个变量比较有实用性,一般cd命令只能打开当前目录下的目录,打开其他目录需要输入全路径,对于某些 较为常用的目录,我们可以把它加入到$CDPATH中,设置方法和$PATH一致,这样cd就可以直接搜索到该目录的子目录 > cd etc > -bash: cd: etc: No such file or directory > export CDPATH=$CDPATH:/usr/local;cd etc > /usr/local/etc
下面详细介绍一下$DIRSTACK变量,目录栈,从这里发现学的越多越发现有趣的东西越多,先讲下栈是什么。
栈(stack)又名堆栈,是一种运算受限的线性表,其限制是仅允许表的一端进行插入和删除运算,这一端被称为栈顶,另一端被称为栈底。向一个栈插入新元素又称为进栈、入栈或者压栈,把新元素放到栈元素的上面,成为新的栈顶元素。从一个栈删除元素又称为出栈或者退栈,就是把栈顶的元素删除掉,使其相邻的元素成为新的栈顶元素,栈的数据进出规则总结就是先进后出,后进先出。那目录栈就比较好理解了就是叠罗汉一样,一层一层的把新的目录压入栈内,成为新栈,用pushd命令可以实现压栈,用pushd +N或者pushd -N可以实现栈内目录的切换,+是正向选取,-是逆向选取,第一个元素是0,除了选取+0外,其他的位置栈内数据都会向左循环,栈底变栈顶。
popd命令可以删除目录栈中的元素,+N和-N和pushd的用法一致。
目录栈可以用于多个目录的切换,下面演示一下。
pushd /usr/local/bin;pushd /usr/local/lib;pushd /etc > /etc /usr/local/lib /usr/local/bin ~ ##第一个被压入的是~ dirs ##显示所有栈元素 > /etc /usr/local/lib /usr/local/bin ~ pushd +1 ##切换到/usr/local/lib目录下,且整个栈元素左移循环 > /usr/local/lib /usr/local/bin ~ /etc popd +1 ##删除/usr/local/bin这个栈元素
$DIRSTACK即代表当前所在栈元素,即当前目录,和$PWD值一样。
$EUID ##有效用户ID,可以通过su命令进行修改,和UID并不总是一样的 $FUNCNAME ##当前运行函数的函数名,需要注意的是函数外的话该变量为空 funcname_xyz () { echo "$FUNCNAME now executing." } funcname_xyz > funcname_xyz $GROUPS ##存储GID,当用户属于多个GROUP的时候,形成数组 $HOME ##用户home目录 $HOMENAME ##测试为空 $HOSTTYPE ##主机类型,类似$MACHTYPE echo $HOSTTYPE $MACHTYPE > x86_64 x86_64-redhat-linux-gnu $IFS ##内部字符分隔符,缺省值是空白符(空格,制表符以及换行符) $LINENO ##记录了变量本身在脚本中的行数,在进行脚本调试的时候有可能用上,暂时未用过,因为脚本报错有行提醒 $MACHTYPE ##架构
$OLDPWD ##上一个工作目录,cd $OLDPWD同cd - $OSTYPE ##操作系统类型 $PATH ##可执行文件搜索路径,从安全角度考虑,一般是不能把当前路径./加入$PATH变量的 $PIPESTATUS ##数组,每个元素代表管道中相对应的每个命令的退出状态 $PPID ##给出进程的父进程PID
下面介绍一个比较常用的内建变量$PROMPT_COMMAND,用于存储在主提示符$PS1显示之前所需要执行的命令,默认的内容是【printf "\033]0;%s@%s:%s\007" "${USER}" "${HOSTNAME%%.*}" "${PWD/#$HOME/\~}"】,这里【"\033]0;%s@%s:%s\007"】是printf命令的格式输出,后面三个变量字符是填充printf中的三个%s字符的。
这里的默认内容可以进行替换,在linux的管理中,当系统出现异常的时候,需要去查询用户的异常操作,但是linux在多终端登录执行命令的时候有一些奇怪的现象,用同一个账号同时打开tty0和tty1,获取当前的~/.bash_history为h0,这时候tty0执行操作h1,然后关闭tty0,这时候h1会写入~/.bash_history,形成h0+h1,然后tty1执行h2,关闭tty1,这时候h1的记录会消失,~/.bash_history只有h0+h2,这就是为什么经常发现history命令找不到自己执行过的命令,而且用户可以清空自己的bash_history文件和history内容,这样后续就很难去查找用户的操作。
这时候$PROMPT_COMMAND就可以做很多的事情了,$PROMPT_COMMAND变量中设置的命令是在上一条命令结束,下一个提示框显示中间执行的,可以用来记录用户的上一条命令操作,在/etc/profile中加入下面的变量,这个也可能有问题的,用户可以设置$PROMPT_COMMAND变量为空来进行应对,所以这个变量要设置为readonly不能被修改。
export HISTORY_FILE=/var/log/history/`date '+%Y-%m-%d'`.log readonly PROMPT_COMMAND='{ date "+%Y-%m-%d %T ##### $(who am i |awk "{print \$1\" \"\$2\" \"\$5}") #### $(history 1 | { read x cmd; echo "$cmd"; })"; } >> $HISTORY_FILE' export PROMPT_COMMAND
这时候尝试过的小伙伴可能就有问题了,普通用户无法读写这个日志文件,需要给这个日志文件赋予写的属性,修改文件属性是root的权限,如果手动进行文件创建和权限赋予可太麻烦了,我没找到合适的生成文件并赋予默认属性的方法,umask好像是针对用户的,没有深入去了解,最简单的方法还是crontab,当天生成后一天的日志文件并赋予写入的权限,这里普通用户依然可以用>清空文件,这时候还要注意日志文件的累加属性,chattr +a filename.log。哪位好心人看到这有更简单的方法,可以告诉我一下。
其实我还想到了inotify-tools进行监控,触发后赋予权限,后续再研究吧。
$PS1 ##主提示符,即在命令行中显示的提示符 [root@golearning-zzs history]# echo $PS1 > [\u@\h \W]\$ ##这里的格式可以自行百度,对照修改,我也记不得 [root@golearning-zzs history]# $PS2 ##次要提示符,当需要额外输入时出现的提示符。默认显示为 `>`。 $PS3 ##三级提示符,显示在 `select` 循环中,之前关于循环的介绍中选择列表选项的时候用到的 $PS4 ##四级提示符,当使用 `-x [verbose trace]`,调用脚本时显示的提示符。默认显示为 `+` $PWD ##当前目录 $REPLY ##read命令默认接收参数,同样适用于select命令 read > xxx echo $REPLY > xxx
本篇已经写的很长了,后面继续分享剩下的内建变量和其他变量相关的知识,慢慢磨。