Shell编程基础

前言

Shell 既是一种脚本编程语言,也是一个连接内核和用户的软件。Shell 将内核、程序和用户连接了起来。
任何代码最终都要被“翻译”成二进制的形式才能在计算机中执行。

有的编程语言,如 C/C++、Pascal、Go语言、汇编等,必须在程序运行之前将所有代码都翻译成二进制形式,也就是生成可执行文件,用户拿到的是最终生成的可执行文件,看不到源码。

这个过程叫做编译(Compile),这样的编程语言叫做编译型语言,完成编译过程的软件叫做编译器(Compiler)。

而有的编程语言,如 Shell、JavaScript、Python、PHP等,需要一边执行一边翻译,不会生成任何可执行文件,用户必须拿到源码才能运行程序。程序运行后会即时翻译,翻译完一部分执行一部分,不用等到所有代码都翻译完。

这个过程叫做解释,这样的编程语言叫做解释型语言或者脚本语言(Script),完成解释过程的软件叫做解释器。

编译型语言的优点是执行速度快、对硬件要求低、保密性好,适合开发操作系统、大型应用程序、数据库等。

脚本语言的优点是使用灵活、部署容易、跨平台性好,非常适合Web开发以及小工具的制作。

Shell 就是一种脚本语言,我们编写完源码后不用编译,直接运行源码即可。

如今的 IT 服务器领域是 Linux、UNIX、Windows 三分天下。Linux 在服务器上的应用非常广泛,可以用来搭建Web服务器、数据库服务器、负载均衡服务器(CDN)、邮件服务器、DNS服务器、反向代理服务器、VPN服务器、路由器等。

常见的 Shell 有 sh、bash、csh、tcsh、ash 等,常用bash。

基础知识

  • 查看shell: cat /etc/shells

  • 输出shell环境变量: echo $SHELL

  • Shell 通过PS1和PS2两个环境变量来控制提示符格式:
    PS1 控制最外层命令行的提示符格式。
    PS2 控制第二层命令行的提示符格式。
    echo $PS1
    echo $PS2

  • 多行输出:

echo "  
>one  
>two  
>three
>"
  • 外部赋值read
#!/bin/bash
# Author : mozhiyan
# Copyright (c) http://see.xidian.edu.cn/cpp/linux/
# Script follows here:
echo "What is your name?"
read PERSON #read 命令从 stdin 获取输入并赋值给 PERSON 变量
echo "Hello, $PERSON"
  • 变量定义
    脚本语言在定义变量时通常不需要指明类型,直接赋值就可以。Bash shell 在默认情况下不会区分变量类型,即使你将整数和小数赋值给变量,它们也会被视为字符串。
    注意,赋值时变量不加$,且赋值号的周围不能有空格
variable=value  #value不含空白符
variable='value' #不能解析变量和命令
variable="value" #可解析变量和命令

url="http://c.biancheng.net"
website1='test1:${url}' 
website2="test2:${url}"
echo $website1 #test1:${url}
echo $website2 #test2:http://c.biancheng.net

#定义变量时加双引号是最常见的使用场景。
  • 使用变量
    使用一个定义过的变量,只要在变量名前面加美元符号$即可。
author="严长生"
echo $author
echo ${author} #为了帮助解释器识别变量的边界

skill="Java"
echo "I am good at ${skill}Script"

#推荐给所有变量加上花括号{ },这是个良好的编程习惯。
  • 命令结果赋值给变量
variable=`cd ~;ls;pwd`
variable=$(cd ~;ls;pwd)
log=$(cat log.txt)
  • 修改变量
url="test1"
url="test2"
readonly url #设为只读变量
url="test3" #error
  • 删除变量
unset variable
unset url #不能删除只读变量
  • 特殊变量
$0	当前脚本的文件名
$n	传递给脚本或函数的参数。n 是一个数字,表示第几个参数。例如,第一个参数是$1,第二个参数是$2。
$#	传递给脚本或函数的参数个数。
$*	传递给脚本或函数的所有参数。
$@	传递给脚本或函数的所有参数。被双引号(" ")包含时,与 $* 稍有不同,下面将会讲到。
$?	上个命令的退出状态(一般成功返回0,失败返回1),或函数的返回值。
$$	当前Shell进程ID。对于 Shell 脚本,就是这些脚本所在的进程ID。

#$* 和 $@ 的区别
不被双引号(" ")包含时,都以"$1" "$2" … "$n" 的形式输出所有参数。
被双引号(" ")包含时,"$*" 会将所有的参数作为一个整体,以"$1 $2 … $n"的形式输出所有参数;"$@" 会将各个参数分开,以"$1" "$2" … "$n" 的形式输出所有参数。

其他特殊变量和字符:
https://cloud.tencent.com/developer/article/1176708
  • shell替换
# 转义字符  
\\ \a \b \f \n \r \t \v......
#!/bin/bash
a=10
echo -e "Value of a is $a \n"
# -e 表示对转义字符进行替换。如果不使用 -e 选项,将会原样输出:Value of a is 10\n。 
可以使用 echo 命令的 -E 选项禁止转义,默认也是不转义的;使用 -n 选项可以禁止插入换行符。

# 命令替换  
#!/bin/bash
DATE=`date`
echo "Date is $DATE"
USERS=`who | wc -l`
echo "Logged in user are $USERS"
UP=`date ; uptime`
echo "Uptime is $UP"

# 变量替换 
根据变量的状态(是否为空、是否定义等)来改变它的值。
${var:-word}	如果变量 var 为空或已被删除(unset),那么返回 word,但不改变 var 的值。
${var:=word}	如果变量 var 为空或已被删除(unset),那么返回 word,并将 var 的值设置为 word。
${var:+word}	如果变量 var 被定义,那么返回 word,但不改变 var 的值。
${var:?message} 如果变量 var 为空或已被删除(unset),那么将消息 message 送到标准错误输出,可以用来检测变量 var 是否可以被正常赋值。
  • shell运算符
    原生bash不支持简单的数学运算,但是可以通过awk 和 expr等其他命令来实现,expr 最常用。
#!/bin/bash
val=`expr 2 + 2`
echo "Total value : $val"

#1.算术运算符  
+ - * / % == !=

#!/bin/sh
a=10
b=20
val=`expr $a + $b`
echo "a + b : $val"

val=`expr $a \* $b` #需加转义符
echo "a * b : $val"

val=`expr $b / $a`
echo "b / a : $val"

val=`expr $b % $a`
echo "b % a : $val"

if [ $a == $b ]
then
   echo "a is equal to b"
fi

if [ $a != $b ]
then
   echo "a is not equal to b"
fi

#2.关系运算符  
-eq -ne -gt -lt -ge -le
关系运算符只支持数字,不支持字符串,除非字符串的值是数字。

#!/bin/sh

a=10
b=20
if [ $a -eq $b ]
then
   echo "$a -eq $b : a is equal to b"
else
   echo "$a -eq $b: a is not equal to b"
fi

#3.布尔运算符  
!(非) -o(或) -a(与)

#!/bin/sh
a=10
b=20
if [ $a -lt 100 -o $b -gt 100 ]
then
   echo "$a -lt 100 -o $b -gt 100 : returns true"
else
   echo "$a -lt 100 -o $b -gt 100 : returns false"
fi

#4.字符串运算符  
= 
!= 
-z(长度是否为0,0为TRUE) 
-n(长度是否为0,0为FALSE) 
str(字符串是否为空,不为空TRUE)

#!/bin/sh
a="abc"
b="efg"

if [ $a != $b ]
then
   echo "$a != $b : a is not equal to b"
else
   echo "$a != $b: a is equal to b"
fi

if [ -z $a ]
then
   echo "-z $a : string length is zero"
else
   echo "-z $a : string length is not zero"
fi

if [ $a ]
then
   echo "$a : string is not empty"
else
   echo "$a : string is empty"
fi

#5.文件测试运算符  
用于检测 Unix 文件的各种属性:
-e 文件/目录是否存在
-d 是否为目录
-f 是否为普通文件(不是目录和设备文件)
-s 文件是否为空
-r 是否可读
-w 可写
-x 可执行
-b 块特殊文件
-c 字符型特殊文件
-g -k -p -u ......

#!/bin/sh
file="/var/www/tutorialspoint/unix/test.sh"
if [ -e $file ]
then
   echo "File exists"
else
   echo "File does not exist"
fi
  • shell注释
    批量注释:
    0.每行加#(麻烦,但是最保险);
    1.将{}括起来定义为一个函数,不进行调用;
    2.<<EOF ...EOF;
    3.'...' (最简单,但会有风险,如不会注释脚本中本身带有单引号的语句部分,一般临时用用)。

  • shell字符串

#单引号
str='this is a string' #不能内插变量和转义

#双引号
your_name='qinjx'
str="Hello, I know your are \"$your_name\"! \n"

#字符串拼接
your_name="qinjx"
greeting="hello, "$your_name" !"
greeting_1="hello, ${your_name} !"
echo $greeting $greeting_1

#获取字符串长度
string="abcd"
echo ${#string} # 输出4

#提取子字符串
string="alibaba is a great company"
echo ${string:1:4} #输出liba

#查找子字符串
string="alibaba is a great company"
echo `expr index "$string" is` #输出3。查找string中is的索引,实际上以第一个字符i为主,因此为3
  • shell数组
    bash仅支持一维数组。
#定义数组
array=(0 1 2 3 4) #下标从0开始,元素注意是空格
或
array=(
0
1
2
3
4
)
或
array[0]=0  #单独定义各分量
array[1]=1
array[2]=2

#读取数组
${array[index]}
value=${array[2]}

读取数组中所有元素:
${array[*]}
或
${array[@]}

#获取数组长度
与获取字符串长度的方法相同:
length=${#array[@]}
或
length=${#array[*]}

length1=${#array[0]} #获取数组元素长度

命令与结构控制

  • echo命令
    echo后隐式自带换行符\n
echo "\"It is a test\""  #"It is a test"

name="OK"
echo "$name It is a test" #OK It is a test

#如果变量与其它字符相连的话,需要使用大括号{ }
mouth=8
echo "${mouth}-1-2009"  #8-1-2009

echo "OK!\n" #原样输出OK!\n
echo "It is a test" 

echo -e "OK!\n" 
echo "It is a test"  #加入了一个空行

echo -e "OK!\nthis a test"

echo "It is a test" > myfile

echo `date`
  • sprintf命令
    由 POSIX 标准所定义,移植性要比 echo 好。
printf "Hello, Shell\n"  #Hello,Shell 不同于echo,必须显式添加换行符\n

#格式化输出:
printf "%d %s\n" 1 "abc"  # 1 abc 加双引号
printf '%d %s\n' 1 "abc"  #加单引号
printf %s abcdef  #不加引号
printf "%s and %d \n" # and 0 若无参数,%s 用NULL代替,%d 用 0 代替

printf "%-10s %-8s %-4.2f\n" GuoFu female 47.9876
#GuoFu      female   47.99
#%s %c %d %f都是格式替代符,%-10s 指一个宽度为10个字符(-表示左对齐,没有则表示右对齐),任何字符都会被显示在10个字符宽的字符内,如果不足则自动以空格填充,超过也会将内容全部显示出来。
%-4.2f 指格式化为小数,其中.2指保留2位小数。
  • if ... else 语句
    最后必须以 fi 来结尾闭合 if,fi 就是 if 倒过来拼写。
    注意:表达式和方括号[ ]之间必须有空格,否则会有语法错误。
#!/bin/sh
a=10
b=20
if [ $a == $b ]
then
   echo "a is equal to b"
fi

#if ... elif ... fi 语句对多个条件进行判断:

#!/bin/sh
a=10
b=20
if [ $a == $b ]
then
   echo "a is equal to b"
elif [ $a -gt $b ]
then
   echo "a is greater than b"
elif [ $a -lt $b ]
then
   echo "a is less than b"
else
   echo "None of the condition met"
fi

#if ... else 语句也经常与 test 命令结合使用,test 命令用于检查某个条件是否成立,与方括号[ ]类似。
#简写成一行,以命令的方式来运行
if test $[2*3] -eq $[1+5]; then echo 'The two numbers are equal!'; fi;

  • test命令
    用于检查某个条件是否成立,它可以进行数值、字符和文件三个方面的测试。
#数值测试
-eq -ne -gt -ge -lt -le

num1=100
num2=100
if test $[num1] -eq $[num2]
then
    echo 'The two numbers are equal!'
else
    echo 'The two numbers are not equal!'
fi

#字符串测试
= != -z -n

num1=jesse
num2=jessi
if test num1=num2
then
    echo 'The two strings are equal!'
else
    echo 'The two strings are not equal!'
fi

#文件测试  
-e -r -w -x -s -d -f -c -b 

cd /bin
if test -e ./bash
then
    echo 'The file already exists!'
else
    echo 'The file does not exists!'
fi

#与逻辑操作符连用  
! -o(或) -a

cd /bin
if test -e ./notFile -o ./bash
then
    echo 'One file exists at least!'
else
    echo 'Both dose not exists!'
fi
  • case...esac语句
    多分支选择结构,与其他语言中的 switch...case 语句类似。格式如下:
case 值 in
模式1)
    command1
    command2
    command3
    ;;
模式2)
    command1
    command2
    command3
    ;;
*)
    command1
    command2
    command3
    ;;
esac

取值后面必须为关键字in,每一模式必须以右括号结束。取值可以为变量或常数。匹配发现取值符合某一模式后,其间所有命令开始执行直至 ;;。;; 与其他语言中的 break 类似,意思是跳到整个 case 语句的最后。
取值将检测匹配的每一个模式。一旦模式匹配,则执行完匹配模式相应命令后不再继续其他模式。如果无一匹配模式,使用星号 * 捕获该值,再执行后面的命令。

echo 'Input a number between 1 to 4'
echo 'Your number is:\c'
read aNum
case $aNum in
    1)  echo 'You select 1'
    ;;
    2)  echo 'You select 2'
    ;;
    3)  echo 'You select 3'
    ;;
    4)  echo 'You select 4'
    ;;
    *)  echo 'You do not select a number between 1 to 4'
    ;;
esac

实际应用中,常用于传参:运行sh,不加参数,或加-f/-d参数的结果

#!/bin/bash

option="${1}"
case ${option} in
   -f) FILE="${2}"
      echo "File name is $FILE"
      ;;
   -d) DIR="${2}"
      echo "Dir name is $DIR"
      ;;
   *) 
      echo "`basename ${0}`:usage: [-f file] | [-d directory]"
      exit 1 # Command to come out of the program with status 1
      ;;
esac

如shell脚本在流程中传参:

image

  • for循环
    一般格式:
for 变量 in 列表
do
    command1
    command2
    ...
    commandN
done

列表是一组值(数字、字符串等)组成的序列,每个值通过空格分隔。每循环一次,就将列表中的下一个值赋给变量。
in 列表是可选的,如果不用它,for 循环使用命令行的位置参数。

for loop in 1 2 3 4 5
do
    echo "The value is: $loop"
done

for str in 'This is a string'
do
    echo $str
done

#!/bin/bash
for FILE in $HOME/.bash* #显示主目录下以 .bash开头的文件
do
   echo $FILE
done
  • while循环
    while循环用于不断执行一系列命令,也用于从输入文件中读取数据;命令通常为测试条件,命令执行完毕,控制返回循环顶部,从头开始直至测试条件为假。其格式为:
while command
do
   Statement(s) to be executed if command is true
done

示例:

COUNTER=0
while [ $COUNTER -lt 5 ]
do
    COUNTER='expr $COUNTER+1'
    echo $COUNTER
done

while循环可用于读取键盘信息。在生信中,经常用于批量处理同路径下文件:

echo 'type <CTRL-D> to terminate'
echo -n 'enter your most liked film: '
while read FILM
do
    echo "Yeah! great film the $FILM"
done

while read ID
do
    command $ID
done
  • until循环
    与 while 循环在处理方式上刚好相反。一般while循环优于until循环,但在某些时候,也只是极少数情况下,until 循环更加有用。
#!/bin/bash
a=0
until [ ! $a -lt 10 ]
do
   echo $a
   a=`expr $a + 1`
done
  • 跳出循环
    Shell也使用 break 和 continue 来强制跳出循环。
    break立即跳出所有循环
#!/bin/bash
while :
do
    echo -n "Input a number between 1 to 5: "
    read aNum
    case $aNum in
        1|2|3|4|5) echo "Your number is $aNum!"
        ;;
        *) echo "You do not select a number between 1 to 5, game is over!"
            break
        ;;
    esac
done

break n可以指定跳出第几层循环

#!/bin/bash
for var1 in 1 2 3
do
   for var2 in 0 5
   do
      if [ $var1 -eq 2 -a $var2 -eq 0 ]
      then
         break 2 #当var1等于2,且var2等于0时,直接跳出外层循环
      else
         echo "$var1 $var2"
      fi
   done
done

continue跳出当前循环

#!/bin/bash
while :
do
    echo -n "Input a number between 1 to 5: "
    read aNum
    case $aNum in
        1|2|3|4|5) echo "Your number is $aNum!"
        ;;
        *) echo "You do not select a number between 1 to 5!"
            continue
            echo "Game is over!" #永远不会执行
        ;;
    esac
done


#!/bin/bash
NUMS="1 2 3 4 5 6 7"
for NUM in $NUMS
do
   Q=`expr $NUM % 2`
   if [ $Q -eq 0 ]
   then
      echo "Number is an even number!!" #偶数
      continue
   fi
   echo "Found odd number"
done

continue 后面也可以跟一个数字,表示跳出第几层循环。一般不会这么用。

  • shell函数
    Shell 函数必须先定义后使用。定义格式:
function_name () {
    list of commands
    [ return value ]
}

Shell 函数返回值只能是整数,一般用来表示函数执行成功与否,0表示成功,其他值表示失败。默认最后一条命令运行结果作为返回值。
如果一定要让函数返回字符串,那么可以先定义一个变量,用来接收函数的计算结果,脚本在需要的时候访问这个变量来获得函数返回值。
调用函数只需要给出函数名,不需要加括号。

#!/bin/bash
funWithReturn(){
    echo "The function is to get the sum of two numbers..."
    echo -n "Input first number: "
    read aNum
    echo -n "Input another number: "
    read anotherNum
    echo "The two numbers are $aNum and $anotherNum !"
    return $(($aNum+$anotherNum)) #需要两个括号
}
funWithReturn
# Capture value returnd by last command
ret=$?
echo "The sum of two numbers is $ret !"

函数嵌套

#!/bin/bash
# Calling one function from another
number_one () {
   echo "Url_1 is http://see.xidian.edu.cn/cpp/shell/"
   number_two
}
number_two () {
   echo "Url_2 is http://see.xidian.edu.cn/cpp/u/xitong/"
}
number_one

删除函数:
unset .f function_name

  • shell函数参数
    在函数体内部,通过$n的形式来获取参数的值,例如,$1表示第一个参数,$2表示第二个参数...
#!/bin/bash
funWithParam(){
    echo "The value of the first parameter is $1 !"
    echo "The value of the second parameter is $2 !"
    echo "The value of the tenth parameter is $10 !"
    #注意,$10 不能获取第十个参数,获取第十个参数需要${10}。当n>=10时,需要使用${n}来获取参数。
    echo "The value of the tenth parameter is ${10} !"
    echo "The value of the eleventh parameter is ${11} !"
    echo "The amount of the parameters is $# !"  # 参数个数
    echo "The string of the parameters is $* !"  # 传递给函数的所有参数
}
funWithParam 1 2 3 4 5 6 7 8 9 34 73
  • shell输入输出重定向
    默认从标准输入设备(stdin)获取输入,将结果输出到标准输出设备(stdout)显示。一般情况下,标准输入设备就是键盘,标准输出设备就是终端,即显示器。
    输出重定向:
command >file

who > users
echo line 1 >users #输出重定向会覆盖文件内容
echo line 2 >> users #不希望文件内容被覆盖,可以使用>>追加到文件末尾

输入重定向:
从文件读取内容。

wc -l users  #2 users。会输出文件名
wc -l < users  #2。不会输出文件名,因为它仅仅从标准输入读取内容

重定向符号:
标准输入文件(stdin):stdin的文件描述符为0,Unix程序默认从stdin读取数据。
标准输出文件(stdout):stdout 的文件描述符为1,Unix程序默认向stdout输出数据。
标准错误文件(stderr):stderr的文件描述符为2,Unix程序会向stderr流中写入错误信息。

command 2 > file  #stderr重定向到file
command 2 >> file
command > file 2>&1 #将stdout和stderr合并后重定向到file
command >> file 2>&1
command < file1 >file2 #对stdin和stdout都重定向,将stdin重定向到file1,将stdout重定向到file2。 

Here document(嵌入文档)
Shell 中的一种特殊的重定向方式。形式如下:

command << delimiter
    document
delimiter

它的作用是将两个delimiter(后一个顶格写)之间的内容(document) 作为输入传递给 command。

wc -l << EOF
    This is a simple lookup program
    for good (and bad) restaurants
    in Cape Town.
EOF

/dev/null文件
如果希望执行某个命令,但又不希望在屏幕上显示输出结果,那么可以将输出重定向到 /dev/null。 /dev/null 是一个特殊的文件,写入到它的内容都会被丢弃;如果尝试从该文件读取内容,那么什么也读不到。但是 /dev/null 文件非常有用,将命令的输出重定向到它,会起到”禁止输出“的效果。

command > /dev/null

command > /dev/null 2>&1  #屏蔽 stdout 和 stderr
  • shell调用
    像其他语言一样,Shell 也可以包含外部脚本,将外部脚本的内容合并到当前脚本。
. filename (点号后有空格),更常用。  
或 source filename

示例:

#被调用脚本 subscript.sh内容:
url="http://see.xidian.edu.cn/cpp/view/2738.html"

#主文件 main.sh内容:
#!/bin/bash
. ./subscript.sh
echo $url

#执行:
chomd +x main.sh
./main.sh

注意:被包含脚本不需要有执行权限。

Ref: http://c.biancheng.net/cpp/shell/

原文地址:https://www.cnblogs.com/jessepeng/p/11204314.html