shell学习入门

花了大概一天半的时间比较全面的接触了一下shell脚本,对于这种语言我对自己的要求很简单,不求深度精通,能写工具类脚本,能阅读别人的脚本,能在在相应场合无障碍沟通就好。这里整理一下这个阶段的知识结构,准备再花一到两天时间接触一下一些高级玩法,到时候Shell就算完篇了。


Shell 也是一门编程语言,学习编程语言我觉得需要关注这么一些点:

  • 数据类型

  • 语法/ 结构控制

  • 内置函数和变量

只要相应的关注整理一下,入门还是很快的,我相信读完这篇文章以后,你基本上可以认为自己能独立读写一些简单的Shell脚本,可以有底气的说自己是一个初级的Sheller了。

变量声明

首先要了解一下Shell的变量声明,引用等语法规则。有两个command(declare,local)与变量的声明有关:

1
2
3
declare  a=1  #声明一个全局变量
local b="hello~~" #声明一个局部变量,一般在函数体中使用
c=1 #实际情况是,这两个command可用可不用,不使用同样可以声明变量

变量的声明特别要注意赋值等号(=)两边不能有空格。

1
a = 2  #这个声明就会失败!!!

变量声明了以后还有一个变量读取,Shell读取变量的语法如下:

1
${var} 或者 $var #作为一个资深Coder,当然知道前者更专业,可以避免在字符串中解析出错

数据类型

shell 不像那些严谨的编程语言有完整的数据体系,我现在接触到的就这么几种:

  1. 字符串
  2. 数值(整形/浮点)
  3. 数组(简单数组/关联数组)

字符串

这个是最常用也是最简单的数据类型了,首先来感受一下

1
2
3
echo "hello,shell~~" #输出字符串到控制台
echo "hello, shell " > a.txt #将字符串输入到文件a.txt中,输入之前会清空文件
echo "hello,shell" >> a.txt #将字符串输入到文件a.txt中,输入之前不清空文件

和其他的编程语言一样,字符串也有相应的一些操作,整理下来基本上就是这么一些:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
取字符串长度 :  ${#string}

子串截取:
${string:position:length} 从起始位置截取指定长度
${string:position} 从起始位置截取到末尾

子串替换:
${string/substring/replacement} 替换第一个匹配的子串
${string//substring/replacement} 替换所有子串

子串删除
${string%substring} 从变量$string的结尾, 删除最短匹配$substring的子串
${string%%substring} 从变量$string的结尾, 删除最长匹配$substring的子串
.....

数值

数值分为整形和浮点型,他们的操作完全不一样,所以得分开说。
数值类型的话关注点就在运算了,Shell对于整形的运算有三种方式,如下:
** let **

1
2
3
4
a=1;
b=2;
let c=a+b;
echo $c;

** [ ] 中括号**

1
2
3
4
a=1;
b=2;
c=$[a+b];
echo $c;

** (( )) 双小括号 **

1
2
3
4
a=1;
b=2;
c=$(( $a+$b));
echo $c;

*这里总结一下, 这三种方式还是有一些细微的差别的,let 方式最为简单。[ ]需要注意在外面要用$对返回值进行引用。 (( ))最为复杂容易出错,首先在括号内变量的必须要加$引用,其次左括号必须和变量间有空格。 *

bash 不支持浮点运算,如果需要进行浮点运算,需要借助bc,awk 处理。

1
2
c=$(echo "5.01-4*2.0"|bc)
c=$(awk 'BEGIN{print 7.01*5-4.01 }')

bc,awk我暂时也还没有接触到,先放到这吧。todo

数组

数组的定义(初始化)比较简单:

1
2
3
4
5
arr=(0 1 2 3 4 5 6 7 8 9);
或者
arr[0]=0;
arr[1]=1;
arr[2]=2;

下面就是关于数组的一些操作了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#循环打印数组元素
for i in ${arr[@]} ; do
echo $arr[i]
done;
#这里牵涉到了两个知识点:
#(1)${arr[@]} 表示数组中的所有元素
#(1)$arr[i] 获取数组元素的形式,其中i为索引

#顺便说了for结构,对于for结构也可以这样:
for i in 1 2 3 4 5 ;do
echo $i
done;

# 还可以这样:
for((i=0;i<=10;i++));do
echo $i;
done;

#回到数组,获取数组的长度:
${#arr[@]}

数组的基本操作就是这些了,这里所说的数组都是简单数组,它的索引只能是数值,关联数组的不同之处在于索引可以是其他类型如字符串。需要bash4.0以上才支持

1
2
declare -A phone
phone=([jim]=135 [tom]=136 [lucy]=158)

数据类型基本上就是这些了,接下来看看一些常见的结构控制及语法。我们都知道程序的结构有三种:

  • 顺序结构

  • 选择结构

  • 循环结构

就从这个结构来入手语法吧。

语法

顺序结构就没什么好说的了,重点是后面两种。

选择结构

选择结构说白了就是会存在逻辑判断,Shell里面也提供两种大结构来支持判断。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# ------数字比较--------
if test 100 -gt 50 ;
then
ehco " 100 大于50";
else
ehco "100 小于50"
fi
# ------字符串比较--------
if test a \> b ;
then
ehco " a 大于b";
else
ehco "a 小于b"
fi

#------case 子句----------
case "$Keypress" in
[[:lower:]] ) echo "Lowercase letter";;
[[:upper:]] ) echo "Uppercase letter";;
[0-9] ) echo "Digit";;
* ) echo "Punctuation, whitespace, or other";;
esac

上面只是简单的示例,现在来说说具体的语法。对于if结构,后面的比较操作有三种写法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 1. test
if test 100 -gt 50 ;
then
ehco " 100 大于50";
else
ehco "100 小于50"
fi

# 2. [ ]中括号
if [ a \> a ]
then
echo "a>a"
else
echo "a<a"
fi;

# 3. [[ ]] 双中括号
if [[ a == a && 2 > 3 ]]
then
echo " all true";
else
echo " not all true";
fi;

总结一下,这三种写法中 test, [ ] 最为复杂,对于<,>等操作符必须要要转义\>, \<,而且不支持多条件判断,所以一般情况下就用双中括号[[ ]]来得简单。

shell中提供的支持判断的操作符比较别扭,分别正对字符串,数值比较各有一套:
字符串比较

1
2
3
4
5
6
> , >=  : 大于,大于等于
<, <= : 小于,小于等于
== : 等于
!= : 不等于
-z : 字符串为空
-n : 字符串非空

数值比较

1
2
3
4
5
6
-eq   等于
-ne 不等于
-lt 小于
-le 小于等于
-gt 大于
-ge 大于或等于

文件比较

1
2
3
-e  文件是否存在      
-f 是一个标准文件
-d 是一个目录

个人感觉这个定义好别扭,字符串和数值完全没有必要弄两套比较体系,而且数值比较很直观的东西变得很复杂。接下来,最蛋痛的事情到了,上面说到if后面的写法有三种:test , [] , [[ ]]。其中test, [] 是严格按照比较操作符的定义来的,如果是字符串比较,只能用符号比较>,<等,如果是数值比较,只能用文字比较符 -gt ,-lt等,但是对于[[ ]] 无论是文字比较还是数值比较,两种比较符都可以任意使用。所以一般建议只用[[]],你以为这就完了?不,还有一种情况,三目运算:

1
2
echo $(( 3 > 2?100:999));
echo $(( a > b?101:991));

三目运算表达式只能用符号比较符 > ,< 等!!!
在这一段的学习过程中我是感觉非常混乱,绕了好久才把这些点给理清楚,现在统一总结一下:

  1. test , [] , [[ ]]三种结构中,test ,[] 需要对<,>等转义。而且两种比较符各司其职,不能跨界操作。而 [[]]不需要转义,比较符号也可以跨界操作,非常方便。所以一般就只用[[]].
  2. 三目运算表达式中的比较只能用符号比较符 >, <.

Case子句的写法就没这么纠结了,当然在我看来,语法还是很蛋痛的。来记录一下它的语法规则:

1
2
3
4
5
6
case "$Keypress" in  
[[:lower:]] ) echo "Lowercase letter";;
[[:upper:]] ) echo "Uppercase letter";;
[0-9] ) echo "Digit";;
* ) echo "Punctuation, whitespace, or other";;
esac
  1. 条件判断分支必须用右括号 )结束。
  2. 条件子句必须用两个分号;;结束。

循环结构

Shell的循环结构比较简单,就是for, while。都来看看吧

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
#1. for 指定数组
for loop in 1 2 3 4 5 6
do
echo $loop
done

#2. for 变量自增长
for i in `seq 100`
do
echo $i;
done


#3. for 循环已定义数组
arr=(1 2 3 4 5 6 7 8 9 0)
for i in ${arr[@]} ;do
echo $i;
done;

#4. 指定范围
for i in {1..10} ;do
echo $i;
done;

#45. while (( ))
COUNTER=0
while(($COUNTER<=5))
do
echo $COUNTER
let "COUNTER++";
done

#5. while [[ ]]
while [[ $num != 4 ]]
do
echo $num;
let "num++";
done;

循环结构远没有选择结构那么复杂,基本上看看这些示例就OK了。

函数

语法部分还有一个比较重要的方面就是函数了。函数的定义和其他语言一样:

1
2
3
4
5
6
7
8
9
function myfun(){
echo "共传入了:$# 个参数,分别是:"
for i in $*; do
echo $i
done
}
# 1. $# 参数个数
# 2. $* 所有参数的数值
#3. function 可写可不写

Shell似乎不支持带参函数,所以如何在函数内获取参数呢?

1
2
3
4
5
6
function myfun(){
echo ${1};
echo ${2};
echo ${3};
}
#按照索引来获取参数就好了。注意,索引从1开始,${0}获取的是脚本名。

函数的执行也和其它程序不一样,来看一个例子:

1
2
3
4
5
6
7
8
sum=0
function myadd(){
for i in $*; do
let sum+=$i
done
}
myadd 1 2 3 4 # 执行函数
echo $sum;

如果函数有返回值,则可以直接赋值:

1
2
3
4
5
6
7
8
function myadd(){
for i in $*; do
let sum+=$i
done
return $sum;
}
$(myadd 1 2 3 4) # 执行函数
num=$? #获取返回值

到此为止,基本上语法就都走了一遍,接下来要了解一些shell内置的一些变量,命令等,这个是真正能玩转起来的重要因素,看看有哪些东西是需要关注的吧。

内置变量及命令

首先来了解一下一些特殊的内置变量

1
2
3
4
5
6
7
8
9
10
11
12
13
$0  脚 本名字
$1- $9 位置参数 #1 - #9
${10} 位置参数 #10
$# 位置参数的个数
"$*" 所有的位置参数(作为单个字符串) *
"$@" 所有的位置参数(每个都作为独立的字符串)
${#*} 传递到脚本中的命令行参数的个数
${#@} 传递到脚本中的命令行参数的个数
$? 返回值
$$ 脚本的进程ID(PID)
$- 传递到脚本中的标志(使用set)
$_ 之前命令的最后一个参数
$! 运行在后台的最后一个作业的进程ID(PID)

对于初次接触者,可能这么几个要重点关注下

1
2
3
4
5
6
7
8
9
10
11
12
# 1. 位置参数,在上面的示例中已经用过,函数内部使用传递参数的写法
$1 - $9 位置参数 #1 - #9
${10} 位置参数 #10

# 2. 参数的数量
$# 位置参数的个数

#3. 参数的集合,在一些不关心具体参数值的函数中特别有用,例如 add
$@ 所有的位置参数(每个都作为独立的字符串)

#4. 函数的返回值, 不能直接将函数执行后复制给变量,必须通过这个返回值变量
$? 返回值

命令的话太多了,这里就不纠结这个话题,通篇下来,shell的初步接触应该比较完备了,后续再研究下高端玩法。