Shell编程

Shell类型

shell种类特别的多,一般在Linux下默认都是bash,我们可以通过cat /etc/passwd查看每个用户默认的shell。同时我们在工作中可能会用到zsh,其实zsh是兼容bash的。

一般在shell下执行的命令有两种:

  • 内建命令
  • 非内建命令

内建命令在执行时一般不会fork出子shell,内建命令有cd、alias、exit,一般我们通过which cmd可以查出命令是不是内建命令。不管是内建命令,还是非内建命令,我们都可以通过$?来获取命令的返回码(返回吗一般是0表示成功,非0表示失败)。

Shell如何执行命令

执行命令我们一般是分单条执行还是多条执行。

单条执行

其实就是我们常用的方式,只不过是如果是内建命令就不会fork而已,非内建命令会fork/exec/wait

多条执行

其实就是我们说写的脚本(批处理),把多条命令放到一个文件中,然后一块执行。
比如有如下一个脚本,文件名为:script.sh

#! /bin/sh

cd ..
ls
复制代码

一般有两种执行方式:

  1. 以一种可执行文件的方式来执行。

chmod +x script.sh然后./script.sh就可以执行了。这种执行方式的大概流程为:先fork出一个子shell,在子shell执行通过exec来执行该脚本中第一行所指定的/bin/sh的解释器(相当于将sh的代码段加载到这个子进程中来),而当前script.sh是作为一个命令行参数来传递给sh的。当执行到cd ..,由于cd是内建命令,相当于sh内部调用一个函数来改变当前子shell的工作路径;然后再执行ls,由于ls不是内建命令,所以会再fork出一个子进程执行ls,子shell调用wait等待ls进程执行完,当ls的进程执行完之后,子shell的wait会解除,由于脚本中的代码已经执行完了,所以主shell中wait子shell也解除,因此我们就能看到shell提示符了。

  1. 直接通过解释器来执行

也就是这种sh ./script.sh方式。我们会发现,其实两种方式的本质还是一样的。
我们应该能发现在脚本中有一句cd ..,但是脚本执行完之后,我们的工作目录本没有任何改变,这是为啥?这是因为修改的是子shell的工作目录,当前主shell的目录并没有修改呀。

  1. 执行脚本时能不能先不要fork出子shell

答案肯定是可以的。比如我们执行以下的方式source ./script.sh或者. ./script.sh,这种方式就不会先fork出子shell来执行,但是到单个命令时还是会fork。这里可以联想到我们平时在定义环境变量时脚本,我们执行该脚本时都用source script.sh的原因。因为执行不会fork,所以环境变量就是针对当前的shell进程的;如果执行之前的方式执行,仅仅修改的子shell的环境变量,主shell环境变量并没有任何变化。

另外要注意,通过(cd ..; ls -l)这样也是会fork的。

Shell基本语法

变量

在shell中变量分成本地变量、环境变量。本地变量只能在当前的shell进程中使用,而环境变量可以在当前的shell进程的子进程中使用。

  1. 本地变量

本地变量的定义方式:

VALNAME=value
复制代码

等号两边不能有空格。获取获取变量的值呢:

echo ${VALNAME}
复制代码

在shell中所有变量的类型都是字符串类型,如果变量没有定义就是空字符串。

  1. 环境变量

将本地变量导出就是环境变量,可以通过命令export VALNAME。也可以一边定义一边导出:

export AAA=value
复制代码

查看环境变量,可以通过env或者printenv

  1. 如何使用变量

使用变量推荐使用:

echo ${SHELL}
复制代码

也就是大括号的方式。这样的好处就是后边跟一些字符串,变量展开以后也是可以拼接上的。比如echo ${SHELL}abc,那么结果就是/bin/zshabc

文件名代换globbing

其实就是一些通配符。

通配符 作用
* 匹配0个或者多个任何字符
匹配1个任意字符
[] 匹配方括号中其中一个字符
比如我们执行ls ch1[0-2].doc,那么shell其实会先展开通配符,比如能找到ch10.docch11.doc,然后将这两个文件传递给ls。也就是说ls并不会不处理这些通配符,是由shell先展开再传递给ls。

命令行代换``、$()

通过``或者$()括起来的也是一条命令,shell会先执行这条命令,将执行结果放到当前所在的命令行中。

DATE=`date`
echo $DATE

或者
DATE=$(date)
echo $DATE
复制代码

算术代换:$(())

shell中变量默认都是字符串,所以可以涉及到整形变量+-*/可以这样处理:

val=10
echo $(($val + 10))
复制代码

$(())只能用于整形运算

转义字符\

有一些特殊的字符如果我们想使用这些特殊字符的字面量时,就可以使用转义。
echo \$SHELL结果就不打印SHELL变量了,而是打印$SHELL

另外\也可以表示续行的意思。

单引号、双引号

单引号表示字符的字面值。双引号一般情况下都表示字符的字面值,但是在遇到$变量名则会展开变量值;在遇到``则会命令替换。

bash启动脚本

就是bash启动时执行的脚本,这里边的规则还是挺复杂的,但是只需要记住一条,我们可以将环境变量、alias、mask定义到.bashrc文件中即可。这样在bash启动时会自动source启动脚本,这样我们预先定义的变量就自动生效了。

shell脚本语法

条件测试

条件测试语句在shell中有两种表达[test。比如

VAR=2
test $VAR -gt 1$

[ $VAR -gt 3 ]
复制代码

不管用那种方式,最终表示条件是真还是假,是通过$?来判断的,0表示true,1表示false。

AAA_test.png

对于与或非

AAAA_1.png

判断语句

在shell编程中是可以使用if语句的,但是跟我们的c语言的if也不太一样。

if [ -f ~/.bashrc ]; then 
    . ~/.bashrc
fi
复制代码

其实涉及三条语句,if [ -f ~/.bashrc ] 如果测试语句$?为0则表示为true,否则为false。then . ~/.bashrc在shell一般一行只有一条语句,如果要有多条语句的话,就用;分离。fi 表示if的结束标记。

特殊if语句,也就是if的条件永远为true。:是空语句,执行结果$?是0。

if :; then echo "always true"; fi
复制代码

常见的if用法为:

num=1

if [ $(($num)) -gt 10 ]; then
    echo 'a'
elif [ ${num} -eq 10 ]; then
    echo 'b'
else
    echo 'c'
fi
复制代码

&&||用法,其实就是利用短路特性。

test "$(whoami)" != 'root' && (echo you are using a non-privileged account; exit 1)
复制代码

case语句

case语句就是c语言中的switch case。大概的用法是这样:

case $1 in 
    start) 
    ... 
    ;; 
    stop) 
    ... 
    ;; 
    reload | force-reload)
     ... 
     ;; 
     restart) 
     ...
     ;;
     *) 
     log_success_msg "Usage: /etc/init.d/apache2 {start|stop|restart|reload|force-reload|start-htcacheclean|stop-htcacheclean}" 
     exit 1 
     ;;
esac
复制代码

以case开头,在in里边进行匹配,支持通配符,一旦找到某一个条件就执行对应的语句最终语句是以;;结束。整个语句是以esac结束。

for/do/done

for循环。比如

for FRUIT in apple banana pear; do 
    echo "I like $FRUIT"
done
复制代码

如果想将某一个目录下的文件改名,可以这样:

files=`ls test`
echo ${files}

for f in ${files}; do
   mv "test/${f}" "test/${f}.tmp"
done
复制代码

while/do/done

COUNTER=1
while [ "$COUNTER" -lt 10 ]; do
   echo "Here we go again" 
   COUNTER=$(($COUNTER+1))
done
复制代码

这个应该很好理解。

位置参数与特殊变量

可以参考如下列表:

AAAA-2.png

其中位置参数可以用shift命令左移。比如shift 3表示原来的

4现在变成4现在变成

1,原来的

5现在变成5现在变成

2等等,原来的

11、

2、

3丢弃,3丢弃,

0不移动。不带参数的shift命令相当于shift 1。shift可以使用场景就是在脚本内部可能会调用其他的脚本,但是参数又不想用那么多,这个时候就可以使用shift进行丢弃。

函数

shell中的函数不用写参数列表跟返回值类型的。是可以传递参数跟返回值的,只不过不需要声明而已。

is_directory()
{ 
   DIR_NAME=$1 
   if [ ! -d $DIR_NAME ]; then
      return 1 
   else 
      return 0 
   fi
}
for DIR in "$@"; do 
   if is_directory "$DIR" 
      then : 
   else 
      echo "$DIR doesn't exist. Creating it now..." 
      mkdir $DIR > /dev/null 2>&1 
      if [ $? -ne 0 ]; then 
         echo "Cannot create directory $DIR" 
         exit 1 
      fi
   fi
done 
复制代码

在shell脚本中如果要表示返回true,一般用return 0,这个跟if判断是有关系的。另外在shell空语句用:来表达。

debug

image.png