一、shell简介

Shell是用户和Unix/Linux内核沟通的桥梁,用户的大部分工作都是通过Shell完成的。Shell既是一种命令语言,又是一种程序设计语言。作为命令语言,它交互式地解释和执行用户输入的命令;作为程序设计语言,它定义了各种变量和参数,并提供了许多在高级语言中才具有的控制结构,包括循环和分支。

    它虽然不是Unix/Linux系统内核的一部分,但它调用了系统核心的大部分功能来执行程序、建立文件并以并行的方式协调各个程序的运行。因此,对于用户来说,shell是最重要的实用程序,深入了解和熟练掌握shell的特性极其使用方法,是用好Unix/Linux系统的关键。

    Shell 脚本: 其实就是命令的堆积。

Shell有两种执行命令的方式

  • 交互式(Interactive):解释执行用户的命令,用户输入一条命令,Shell就解释执行一条。

  • 批处理(Batch):用户事先写一个Shell脚本(Script),其中有很多条命令,让Shell一次把这些命令执行完,而不必一条一条地敲命令。

    shell只定义了一个非常简单的编程语言,所以,如果你的脚本程序复杂度较高,或者要操作的数据结构比较复杂,那么还是应该使用Python、Perl这样的脚本语言,或者是你本来就已经很擅长的高级语言。因为sh和bash在这方面很弱,比如说:

  • 它的函数只能返回字串,无法返回数组

  • 它不支持面向对象,你无法实现一些优雅的设计模式

  • 它是解释型的,一边解释一边执行,连PHP那种预编译都不是,如果你的脚本包含错误(例如调用了不存在的函数),只要没执行到这一行,就不会报错

二、第一个shell脚本

    Shell脚本典型的开发周期:直接在命令行(command line)上测试。然后,一旦找到能够完成工作的适当语法,再将它们放进一个独立的脚本里,并为该脚本设置执行的权限。

1、编写

    打开文本编辑器,新建一个文件,扩展名最好为sh(sh代表shell),虽然扩展名并不影响脚本执行,但是便于我们识别,而且对于编辑器来说,还可能进行语法高亮。

#!/bin/bash

# print the "hello world"
echo "Hello World"

2、执行

运行Shell脚本有两种方法:

  • 作为可执行程序

chmod +x ./first.sh      # 使脚本具有执行权限
./first.sh               # 执行脚本

注意:一定是 ./first.sh。为什么呢?通常我们在执行命令的时候,shell会根据PATH变量定义的路径进行查找,而我们的当前路径一般不会加入PATH变量,所以就会提示 -bash: first.sh : command not found。

./first.sh  是通过相对路径,指明执行 当前目录下的叫做 first.sh 的文件。

  • 作为解释器参数

/bin/bash  first.sh

 3、运行原理

    当shell执行一个程序时,会要求UNIX内核启动一个新的进程(process),以便在该进程里执行所指定的程序。内核知道如何为“编译型”程序做这件事。shell脚本并非编译型程序;当shell要求内核执行它时,内核将无法做这件事,并回应“not executable format file”错误信息。shell收到此错误信息时,就会认为“这不是编译型程序,那么一定是shell脚本”,”退回到shell”接着会启动一个新的/bin/sh 副本来执行该程序。

    当系统只有一个shell时,“退回到/bin/sh”的机制非常方便。但现行的系统都会拥有好几个shell,因此需要通过一种方式,告知内核应该以哪个shell来执行所指定的shell脚本。事实上,这么做有助于执行机制的通用化,让用户得以直接引用任何的程序语言解释器,而非只是一个命令shell。方法是,通过脚本文件特殊的第一行来设置:在第一行的开头处使用 #!这两个字符(必须:顶行&&顶头)。


    在计算机科学中,sha-bang是一个由井号和叹号构成的字符串行(#!),其出现在文本文件的第一行最前两个字符。 在文件中存在sha-bang的情况下,类Unix操作系统的程序载入器会分析sha-bang后的内容,将这些内容作为解释器指令,并调用该指令,并将载有sha-bang的文件路径作为该解释器的参数[1]。#! (magic number)为了让Linux内核识别这是什么格式的文件。

* * *

    The sha-bang ( #!)  at the head of a script tells your system that this file is a set of commands to be fed to the command interpreter indicated. The #! is actually a two-byte magic number, a special marker that designates a file type, or in this case an executable shell script (type man magic for more details on this fascinating topic). Immediately following the sha-bang is a path name. This is the path to the program that interprets the commands in the script, whether it be a shell, a programming language, or a utility. This command interpreter then executes the commands in the script, starting at the top (the line following the sha-bang line), and ignoring comments. 

#!/bin/sh
#!/bin/bash
#!/usr/bin/perl
#!/usr/bin/tcl
#!/bin/sed -f
#!/bin/awk -f

    Each of the above script header lines calls a different command interpreter, be it /bin/sh, the default shell (bash in a Linux system) or otherwise. Using #!/bin/sh, the default Bourne shell in most commercial variants of UNIX, makes the script portable to non-Linux machines, though you sacrifice Bash-specific features. The script will, however, conform to the POSIX  sh standard.

Note that the path given at the "sha-bang" must be correct, otherwise an error message -- usually "Command not found." -- will be the only result of running the script. 

#! can be omitted if the script consists only of a set of generic system commands, using no internal shell directives. The second example, above, requires the initial #!, since the variable assignment line, lines=50, uses a shell-specific construct. Note again that #!/bin/sh invokes the default shell interpreter, which defaults to /bin/bash on a Linux machine.

  This tutorial encourages a modular approach to constructing a script. Make note of and collect "boilerplate" code snippets that might be useful in future scripts. Eventually you will build quite an extensive library of nifty routines. As an example, the following script prolog tests whether the script has been invoked with the correct number of parameters.


When you execute a program, the kernel checks whether it starts by some magic byte sequence. If the executable file starts with #!, the kernel interprets the rest of the line as an interpreter name. 

    If the executable file starts with \177ELF (where \177 is byte 127), it loads the file as an ELF executable; that's the normal kind on most unix systems nowadays.

    If the kernel doesn't recognize the file format, it refuses to execute the file and returns the error ENOEXEC (Exec format error). When the shell notices that, it takes upon itself to execute the program as a shell script. If the magic line is not provided, a default shell is used to run the script. This default shell could either be Bourne shell (sh) which is the case in some flavors, however, in some other flavors, the default shell used is same as login shell to execute it. The thing is: Don't leave it to the system to decide the shell, always provide the shell which you want in the first line.

[root@skype ~]# file /etc/init.d/sshd
/etc/init.d/sshd: Bourne-Again shell script text executable
[root@skype ~]# file /bin/ls
/bin/ls: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.6.18, stripped


Q. Sha-Bang(#!)的编写有什么规范?

A. Sha-Bang(#!)应该位于脚本的第一行,并且顶格填写,否则都是错的,即使Sha-Bang之前的内容都是注释,这种错误是常见的,而且不易发现的,因为此时Sha-Bang(#!)所在行实际上是不起效的,系统使用了默认的命令行解释器


Q. 为什么推荐这种写法:#!/bin/env perl?

A. 因为这是有利于移植脚本到其它平台的写法,解释器的默认安装路径在各个操作系统是不太一样的,有的是/bin/,有的是/usr/bin/,甚至有可能是用户自定义的路径,使用env就基本上能够通用了。虽然env也有可能在/bin/或者/usr/bin/中,但通常的情况是在这两个路径下都有env,或者其中一个是另一个的符号链接


    用户可以指定命令解释器,而非只是一个命令shell。通过脚本文件中特殊的第一行来设置:在第一行的开头处使用 #! 这两个字符。当一个文件的开头字符是 #! 时,内核会扫描该行的其余部分,看是否存在可用来执行程序的解释器的完整路径。(中间如果出现任何空白符号都会略过),此外,内核还会扫描是否有一个选项(有且只能有一个)要传递给解释器,内核会以被指定的选项来引用解释器。

#! /bin/csh –f         

#! /bin/awk –f (直接通过内核调用awk程序解释后面的脚本,而不是shell间接调用)

假设有一个awk脚本,/usr/test.awk,它的第一行如下: #! /bin/awk –f,如果shell的查找路径(PATH)中有 /usr,当用户键入 test.awk时,内核解释 #! 这行后,便会以如下的方式来引用awk:

         /bin/awk–f  /usr/test.awk

这样的机制让我们得以轻松地引用任何的解释器。

 

4、注释

以”#“开头的行都是注释,会被解释器忽略。

5、输出


    echo命令将参数打印到标准输出(把字符串转换为数据流),参数之间以一个空格隔开,并以换行符号结尾。-n选项会省略结尾的换行符。` ` 或者 $()命令则把数据流转换为字符串。


    printf 命令模仿C程序库里的printf()库函数。它几乎复制了其所有功能。完整的语法分为两部分: printf  “format-string”  [argumnets …]

参数以空白符分隔,如果参数的个数比格式声明的多,则printf会循环且依次的使用格式字符串里的格式声明,直到处理完参数。意味着把参数依次左移处理。这是和C printf函数最大的不同。

三、条件判断

    很多时候,我们都需要进行条件判断,然后对不同的结果产生不同的行为。比如判断 3 是否 大于 2,我们可能想当然像下面这样,在命令行上:

[root@skype tmp]# 3 > 2
-bash: 3: command not found
[root@skype tmp]# ls
2

很遗憾,在命令行上,不能直接这样测试,因为对于 shell 来说, > , < 都是元(meta)字符,具有特殊含义(重定向)。

    为了解决这种尴尬,于是shell专门提供了关于测试的命令: test, [ , ` `, 可以针对 数字、字符串、文件进行测试。可以man bash得到更多的信息,在里面找到对CONDITIONAL EXPRESSIONS的描述。

test 与 [  同样用于条件测试: "["是一个可执行程序,路径是"/usr/bin/[", [ 是一个命令,它是内置命令test的简写形式,只不过它要求最后一个参数必须是 ]在使用[ ] 进行判定的时候有一个事项要注意的是,在括号两边以及符号两边均要有空格。

运算符            描述              示例
## 文件状态测试
-e filename     如果filename 存在,则为真       [ -e /var/log/syslog ]
-d filename     如果filename 为目录,则为真      [ -d /tmp/mydir ]
-f filename     如果filename 为常规文件,则为真    [ -f /usr/bin/grep ]
-L filename     如果filename 为符号链接,则为真    [ -L /usr/bin/grep ]
-r filename     如果filename 可读,则为真       [ -r /var/log/syslog ]
-w filename     如果filename 可写,则为真       [ -w /var/mytmp.txt ]
-x filename     如果filename 可执行,则为真      [ -L /usr/bin/grep ]
filename1 -nt filename2  如果 filename1 比 filename2 新,则为真 [ /tmp/install/etc/services-nt /etc/services ]
filename1 -otfilename2   如果filename1 比filename2 旧,则为真  [/boot/bzImage -ot arch/i386/boot/bzImage ]


## 字符串测试 (请注意引号的使用,这是防止空格扰乱代码的好方法)
-z string        如果 string 长度为零,则为真     [ -z $myvar ]
-n string        如果 string 长度非零,则为真     [-n $myvar ]
string1 = string2   如果 string1 与 string2 相同,则为真 [$myvar = one two three ]
string1 != string2   如果 string1 与 string2 不同,则为真    [$myvar != one two three ]
string1 < string2       如果string1在本地的字典序列中排在string2之前,则为真     [[$myvar < "one" ]]
string1 > string2       如果string1在本地的字典序列中排在string2之后,则为真     [[$myvar > "one" ]]


## 算术测试
num1 -eq num2             等于       [ 3 -eq $mynum ]
num1 -ne num2             不等于      [ 3 -ne $mynum ]
num1 -lt num2             小于       [ 3 -lt $mynum ]
num1 -le num2             小于或等于    [ 3 -le $mynum ]
num1 -gt num2             大于       [ 3 -gt $mynum ]
num1 -ge num2             大于或等于    [ 3 -ge $mynum ]


bash [ ] 单双括号

基本要素:

  [ ] 两个符号左右都要有空格分隔

  内部操作符与操作变量之间要有空格:如  [  “a”  =  “b”  ]

  字符串比较中,> < 需要写成\> \< 进行转义

  [ ] 中字符串或者${}变量尽量使用"" 双引号扩住,避免值未定义引用而出错的好办法

  [ ] 中可以使用 –a –o 进行逻辑运算

  [ ] 是bash 内置命令:[ is a shell builtin

wKiom1USzHfgav_IAACCqQkeLf4090.jpg

bash  [[  ]] 双方括号

基本要素:

  ` ` 两个符号左右都要有空格分隔

  内部操作符与操作变量之间要有空格:如  [[  “a” =  “b”  ]]

  字符串比较中,可以直接使用 > < 无需转义

  ` ` 中字符串或者${}变量尽量如未使用"" 双引号扩住的话,会进行模式和元字符匹配

  [[] ] 内部可以使用 &&  || 进行逻辑运算

  ` ` 是bash  keyword:[[ is a shell keyword

` ` 其他用法都和[ ] 一样


[[  ]] 比[ ] 具备的优势


   ①[[是 bash 程序语言的关键字。并不是一个命令,` ` 结构比[ ]结构更加通用。在[[和]]之间所有的字符都不会发生文件名扩展或者单词分割,但是会发生参数扩展和命令替换。

    ②支持字符串的模式匹配,使用=~操作符时甚至支持shell的正则表达式。字符串比较时可以把右边的作为一个模式,而不仅仅是一个字符串,比如[[ hello == hell? ]],结果为真。` ` 中匹配字符串或通配符,不需要引号。  

    ③使用` `.``.``.` `条件判断结构,而不是[... ],能够防止脚本中的许多逻辑错误。比如,&&、||、<和> 操作符能够正常存在于` `条件判断结构中,但是如果出现在[ ]结构中的话,会报错。  

    ④bash把双中括号中的表达式看作一个单独的元素,并返回一个退出状态码。

       使用` `.``.``.` `条件判断结构, 而不是[ ... ], 能够防止脚本中的许多逻辑错误. 比如,&&, ||, <, 和> 操作符能够正常存在于[[]]条件判断结构中, 但是如果出现在[ ]结构中的话, 会报错。


注意事项:

因为Bash变量不是强类型的,所以你应该小心:
#!/bin/bash

a=4
b=5

#  Here "a" and "b" can be treated either as integers or strings.
#  There is some blurring between the arithmetic and string comparisons,
#+ since Bash variables are not strongly typed.

#  Bash permits integer operations and comparisons on variables
#+ whose value consists of all-integer characters.
#  Caution advised, however.

echo

if [ "$a" -ne "$b" ]
then
  echo "$a is not equal to $b"
  echo "(arithmetic comparison)"
fi

echo

if [ "$a" != "$b" ]
then
  echo "$a is not equal to $b."
  echo "(string comparison)"
  #     "4"  != "5"
  # ASCII 52 != ASCII 53
fi

# In this particular instance, both "-ne" and "!=" work.

echo

exit 0

    As S.C. points out, in a compound test, even quoting the string variable might not suffice. [ -n "$string" -o "$a" = "$b" ] may cause an error with some versions of Bash if $string is empty. The safe way is to append an extra character to possibly empty variables, [ "x$string" != x -o "x$a" = "x$b" ] (the "x's" cancel out).


    各位想过一个问题没有,为什么能够进行测试呢?是根据什么来进行判断的呢?比如 if 语句:

Linux基础:Shell脚本学习_第1张图片

Linux基础:Shell脚本学习_第2张图片

    当我们执行命令后,通常会返回2类值:状态返回码(exit status),以及命令执行返回结果:标准输出或标准错误输出。测试命令是根据  exit status 进行判断的,和 标准输出的信息没有半毛钱关系。

wKioL1US1Czzrb9BAAA38i4QjKw020.jpg

写了一个很2B的程序:

Linux基础:Shell脚本学习_第3张图片

结果无论怎么执行,都是执行的 else 部分。单独执行每个命令,如下

wKiom1US0ybQtRUtAAC46_gsY18694.jpg

wKioL1US1FnwzEO2AABSr76tEKY967.jpg

修改如下即可:

wKiom1US0yahGmjzAABhL42H8pQ383.jpg

 所以:Shell脚本典型的开发周期:直接在命令行(command line)上测试。然后,一旦找到能够完成工作的适当语法,再将它们放进一个独立的脚本里,并为该脚本设置执行的权限。

四、算术运算

大家可以参考:http://mingxinglai.com/cn/2013/01/different-ways-of-doing-arithmetic-operators-in-linux/

6种算术运算方法是:

  1. let operation

  2. expr operation

  3. $[ operation ]

  4. $(( operation ))

  5. 用 awk 做算术运算(算术扩展)

  6. echo "operation" | bc

前面4种shell 脚本进行算术运算的方法, $(()) 最为常用,可以像 C语言 一样流畅的写表达式,所有变量可以不加入:“$”符号前缀。(()) 进行运算, $(()) 取出运算结果。有了它,我们就可以抛弃: let, expr 命令了。

a=$((a+1, b++, --c));
echo $a,$b,$c

但是它们都有一个致命的缺陷,都不支持浮点数Bash仅支持整数运算(直接把小数部分截断(Truncate))。

[root@skype ~]# echo $((10 / 3))
3
[root@skype ~]# echo $((10.3 / 3))
-bash: 10.3 / 3: syntax error: invalid arithmetic operator (error token is ".3 / 3")

这里有一个奇怪的现象:

[root@skype ~]# echo $((10.3 / 3)) &> /dev/null
-bash: 10.3 / 3: syntax error: invalid arithmetic operator (error token is ".3 / 3")

为什么无法进行重定向呢? 因为重定向是 shell把其他进程的输入输出进行重定向。 而这个错误是 bash本身的致命错误,所以当然无法重定向咯。


扯远了,那么此时我们可以通过 bc, awk 来进行更复杂的运算或浮点运算。

bc 交互模式:

[root@skype ~]# bc
bc 1.06.95
Copyright 1991-1994, 1997, 1998, 2000, 2004, 2006 Free Software Foundation, Inc.
This is free software with ABSOLUTELY NO WARRANTY.
For details type `warranty'.
10 / 3            # 未指定精度默认保留整数
3
scale = 6         # 指定精度
10 / 3
3.333333
quit              # 退出交互模式
[root@skype ~]#

bc非交互式:

直接把算术表达式送给bc即可,如果要指定精度,加上scale=N; 用分号隔开

# +, -, *, / , ^
[root@skype ~]# echo 'scale = 2; 9 + 8 * 2 - 6 / 5 + 2^3' | bc
31.80

# functions
[root@skype ~]# echo 'scale = 2; sqrt(15)' | bc
3.87

# 进制转换
[root@skype ~]# echo 'ibase=16; obase = 2; 8' | bc
1000

五、流程控制

=====
for var in item1 item2 ... itemN
do
    command1
    command2
    ...
    commandN
done
# 把 var分别赋值, var=item1, var=item2, ...var=itemN

=====
for file in *
#           ^  Bash performs filename expansion
#+             on expressions that globbing recognizes.

=====
# Missing in [list] in a for loop
for a
do
 echo -n "$a "
done
 
#  The 'in list' missing, therefore the loop operates on '$@'
#+ (command-line argument list, including whitespace).

======
for (( EXP1; EXP2; EXP3 ))
do
    command1
    command2
    command3
done

=====
while condition
do
    command
done

=====
until condition
do
    command
done


========
case "$variable" in 

 "$condition1") 
 command... 
 ;; 

 "$condition2") 
 command... 
 ;; 


esac

# 仅仅是匹配,而没有赋值操作

# 支持文件名 通配
"E" | "e" )
       * )
[[:upper:]]   ) echo "Uppercase letter";;
[0-9]         ) echo "Digit";;
[a-zA-Z]*) return $SUCCESS;;  # Begins with a letter?