Shell编程

756 阅读5分钟

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的目录并没有修改呀。

  2. 执行脚本时能不能先不要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

    1. 变量的类型 在shell里边可以通过declare来声明变量,例如如下代码:
#!/bin/bash 

declare -i mi
declare -i mx=100
declare -i s=0

for((mi=0; mi <= mx; mi=mi+1)); do
	s=s+mi
done

echo $s

同时我们也可以通过declare来声明数组

#!/bin/bash 


declare -a colors
colors[0]="yellow"
colors[1]="white"
colors[2]="black"

echo "len=${#colors[@]}"
for co in ${colors[@]}; do
	echo $co
done
    1. 变量内容的删除与替换 变量内容的删除,最基本的语法为${var#模式}/${var##模式}/${var%模式}/${var%%模式}/${var/old/new}/${var//old/new}
  1. #表示从前往后删除,删除满足要求最短的字符串
file_path="/home/test/workspace/shell/test.cpp"
echo ${file_path#/*/} 

## 输出内容为:
test/workspace/shell/test.cpp
  1. ##表示从前往后删除,删除满足要求的最长字符串
file_path="/home/test/workspace/shell/test.cpp"
echo ${file_path##/*/}
## 输出内容为:
test.cpp
  1. %表示从后往前删除,删除满足要求的最短字符串
file_path="/home/test/workspace/shell/test/test.cpp"
echo ${file_path%/test*}

## 输出内容为:
/home/test/workspace/shell/test
  1. %%表示从后往前删除,满足要求的最长字符串
file_path="/home/test/workspace/shell/test/test.cpp"
echo ${file_path%%/test*}

## 输出结果为:
/home
  1. 使用${var/old/new}替换时,仅仅替换一次。使用${var//old/new}替换时,是替换全部
file_path="/home/test/workspace/shell/test/test.cpp"
echo ${file_path/test/learn}
# 输出:
/home/learn/workspace/shell/test/test.cpp

echo ${file_path//test/learn}
# 输出
/home/learn/workspace/shell/learn/learn.cpp
    1. 变量的测试 这里的内容还挺多的,我这里仅仅记录我常用的。 new_var=${old-content}表示old没有定义时,new_var使用content;如果old定义了并且是个空字符串,那么new_var也是""。另外new_var=${old:-content},也就是说old没有定义还是个空字符串,那么new_var就取content,否则就取old

文件名代换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中[[]][]的拓展,并且[[]]是兼容[]的。竟然是拓展,那功能肯定要比之前的[]要强一些的。

  • 替换掉[]中的-a或者-o
[ -f README.md -a -x README.md ] && echo "yes" || echo "no"
# 如果使用[[]]的话,可以这样:
[[ -f README.md && -x README.md ]] && echo "yes" || echo "no"
  • 使用正则表达式
A="hello"; [[ "$A" =~ hell? ]] && echo "yes" || echo "no"

# 这里要注意=~ 右边不能使用""。如果使用""就表示字符串了,不使用""就表示正则的模式

判断语句

在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)

对于if语句最佳实践,最好对于if [ $var xxxx ]中的$var""括起来。

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

也可以写类似于c语言的循环形式

declare -i mi
declare -i mx=100
declare -i s=0

for((mi=0; mi <= mx; mi=mi+1)); do
	s=s+mi
done

echo $s

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空语句用:来表达。

常见技巧积累

如果脚本遇到错误,最好能暴露出来,不要埋雷

这种情况下,我们一般要不使用set -e或者是指定脚本解释器时,指定-e参数,比如这样:

#!/bin/bash -e

#set -e
mkdir -p a/b
cd a
touch file
cd -

rmdir a
echo "done"

这样脚本一旦遇到error就会停止运行,并且$?也会返回非0

显性指定脚本的工作目录

也就是说,不管使用者从那个位置启动脚本,该脚本都能一如既往的按照预期的目录工作。一般情况下我们将某个脚本放置在某个目录,那一般工作路径基本上是基于该目录的,所以我们最好再写脚本时,在脚本的最开头这样写:

dir=$(dirname $(readlink -f "$0"))
cd ${dir}

这种写法也同时考虑了软连接的情况

脚本的命令行参数处理技巧

最简单的处理,应该是一个for再加一个case语句来处理,复杂点的参数可以通过getopt来实现。

#!/bin/bash 

for arg in $@; do
	case $arg in
		-h)
			echo "usage:xxxx"
			exit 0
			;;
		-a)
			echo "param a handle"
			;;
		*)
			echo "default param handle"
			;;
	esac
done

其他

|命令的右边必须是能接受标准输入作为参数时才能正常运行。比如echo "helloworld" | echo就不会有任何输出,可以通过xargs命令将标准输入转成命令行参数, echo "helloworld | xargs echo就可以正常打印了