菜鸟学shell(一)

简单入门

许多中型,大型的程序都是用编译型语言写成,例如Fortran、C、C++或者java.这类程序只要从源码(source code)转换成目标代码(object code),便能直接通过计算机来执行
编译语言的好处是高效,缺点是:他们呢多半运作于底层,所以处理的是字节、整数、 浮点数.
而脚本编程语言通常是解释型的.这类程序的执行,是由解释器读入程序代码,将其转换成内部的形式再执行.(注意:解释器本身是一般的编译型程序)
脚本语言的好处是它们运行在比编译型语言还高的层级,能够轻易处理文件与目录之类的对象.缺点是:它们的效率通常不如编译型语言.
脚本语言有python,Perl,Ruby与shell.

而shell似乎是各UNIX系统之间通用的功能,并且经过了POSIX的标准化.因此Shell脚本只要“用心写”一次,即可应用到很多系统上.

shell脚本是基于:

  1. 简单性:通过它,可以简洁地表达复杂的操作
  2. 可移植性:可以无需修改在多个系统上执行
  3. 开发容易:可以短期内完成功能强大的脚本

Linux的Shell种类很多,常见的有:

  • Bourne Shell(/usr/bin/sh或/bin/sh)
  • Bourne Again Shell(/bin/bash)
  • C Shell(/usr/bin/csh)
  • K Shell(/usr/bin/ksh)
  • Shell for Root(/sbin/sh)

一般最常用的是bash,(Bourne Again Shell)
在一般情况下,人们并不区分 Bourne Shell 和 Bourne Again Shell,所以,像 #!/bin/sh,它同样也可以改为 #!/bin/bash。

#!告诉系统其后路径所指定的程序即是解释此脚本文件的 Shell 程序。

一个简单脚本

1
2
3
4
5
$ cat > nusers#cat是复制终端的输入到这里建立的文件中去
who | wc -l
^D #Ctrl+D表示end-of-file
$chmod +x nusers #让文件拥有执行的权限
$./nusers

这里前面带$表示终端上的命令
而下面不带$表示要被复制到文件中的代码

这展现了小型Shell脚本的典型开发周期:首先,直接在命令行上测试.然后,一旦找到能够完成工作的适当语法,再将它们放进一个独立的脚本里,并为该脚本设置执行的权限.之后,就能直接使用该脚本了

运行Shell脚本的两种方法

  1. 作为可执行程序
    将上面的代码保存为test.sh,并cd到相应目录
    1
    2
    chmod +x ./test.sh
    ./test.sh

注意,一定要写成 ./test.sh,而不是 test.sh,运行其它二进制的程序也一样,直接写 test.sh,linux 系统会去 PATH 里寻找有没有叫 test.sh 的,而只有 /bin, /sbin, /usr/bin,/usr/sbin 等在 PATH 里,你的当前目录通常不在 PATH 里,所以写成 test.sh 是会找不到命令的,要用 ./test.sh 告诉系统说,就在当前目录找。

  1. 作为解释器的参数
    1
    2
    /bin/sh test.sh
    /bin/bash test.sh

自给自足的脚本:位于第一行的#!

当Shell执行一个程序时,会要求UNIX内核启动一个新进程(process),以便在该进程里执行所指定的程序.内核知道如何为编译型程序做这件事.而如果时我们上述的非编译型程序,内核无法做这种事,但他知道这时非编译型程序,那就一定是脚本程序了.但是之前说的解释器有那么多怎么才能让内核知道是哪个解释器.
方法是在第一行的开头处使用#!这两个字符后面给定解释器的完整路径.
如#! /bin/bash

Shell基本元素

命令与参数

Shell最基本的工作就是执行命令,以互动的方式来使用Shell很容易了解这一点:每键入一道命令,Shell就会执行

1
2
3
4
$cd work; ls -l whizprog.c

$marke
....

以上例子展现了UNIX命令行的原理
命令后面往往会跟着选项,任何额外的参数都会放在选项之后

Shell识别三种基本命令:内建命令、Shell函数以及外部命令

  1. 内建命令就是由Shell本身所执行的命令,有些命令是由于其必要性才内建的,例如cd用来改变目录,read将来自用户(或文件) 的输入数据传给Shell变量
  2. Shell函数是功能健全的一系列程序代码,以Shell语言写成,它们可以像命令那样引用.
  3. 外部命令就是由Shell的副本(新的进程)所执行的命令,基本过程如下:
    a. 建立一个新的进程.此进程即为Shell的一个副本
    b. 在新的进程里,在PATH变量内所列出的目录中,寻找特定的命令.
    c. 在新进程里,以所找到的新进程取代执行中的Shell程序并执行
    d. 程序完成后,最初的Shell会接着从终端读取的下一个命令,或执行脚本里的下一条命令.

    变量

    定义变量时,变量名不加美元符号($,PHP语言中变量需要),如:
    注意,变量名和等号之间不能有空格,这可能和你熟悉的所有编程语言都不一样。同时,变量名的命名须遵循如下规则:
  • 命名只能使用英文字母,数字和下划线,首个字符不能以数字开头。
  • 中间不能有空格,可以使用下划线(_ )。
  • 不能用标点符号
  • 不能使用bash里的关键字

除了显式地直接赋值,还可以用语句给变量赋值,如:

1
2
3
for file in `ls /etc`

for file in $(ls /etc)

以上语句将 /etc 下目录的文件名循环出来。

使用变量
使用一个定义过的变量,只要在变量名前面加美元符号即可,如:

1
2
3
your_name="qinjx"
echo $your_name
echo ${your_name}

这里的变量都是可以重新定义的

1
2
3
4
your_name="tom"
echo $your_name
your_name="alibaba"
echo $your_name

这样写是合法的,但注意,第二次赋值的时候不能写$your_name=”alibaba”,使用变量的时候才加美元符($)。

只读变量
使用 readonly 命令可以将变量定义为只读变量,只读变量的值不能被改变。

下面的例子尝试更改只读变量,结果报错:

1
2
3
4
#!/bin/bash
myUrl="http://www.google.com"
readonly myUrl
myUrl="http://www.runoob.com"

删除变量
使用 unset 命令可以删除变量。语法:

1
unset variable_name

变量被删除后不能再次使用。unset 命令不能删除只读变量。

1
2
3
4
#!/bin/sh
myUrl="http://www.runoob.com"
unset myUrl
echo $myUrl

以上实例执行将没有任何输出。

变量类型
运行shell时,会同时存在三种变量:

1) 局部变量 局部变量在脚本或命令中定义,仅在当前shell实例中有效,其他shell启动的程序不能访问局部变量。
2) 环境变量 所有的程序,包括shell启动的程序,都能访问环境变量,有些程序需要环境变量来保证其正常运行。必要的时候shell脚本也可以定义环境变量。
3) shell变量 shell变量是由shell程序设置的特殊变量。shell变量中有一部分是环境变量,有一部分是局部变量,这些变量保证了shell的正常运行

Shell字符串

字符串是shell编程中最常用最有用的数据类型(除了数字和字符串,也没啥其它类型好用了),字符串可以用单引号,也可以用双引号,也可以不用引号。单双引号的区别跟PHP类似。

  1. 单引号
    1
    str='this is a string'

单引号字符串的限制:

  • 单引号里的任何字符都会原样输出,单引号字符串中的变量是无效的;
  • 单引号字串中不能出现单独一个的单引号(对单引号使用转义符后也不行),但可成对出现,作为字符串拼接使用。
  1. 双引号
    1
    2
    3
    your_name='runoob'
    str="Hello, I know you are \"$your_name\"! \n"
    echo -e $str

输出结果为:
Hello, I know you are “runoob”!
双引号的优点:

  • 双引号里可以有变量
  • 双引号里可以出现转义字符
  1. 拼接字符串

    1
    2
    3
    4
    5
    6
    7
    8
    9
     your_name="runoob"
    # 使用双引号拼接
    greeting="hello, "$your_name" !"
    greeting_1="hello, ${your_name} !"
    echo $greeting $greeting_1
    # 使用单引号拼接
    greeting_2='hello, '$your_name' !'
    greeting_3='hello, ${your_name} !'
    echo $greeting_2 $greeting_3

    输出结果为:

    1
    2
    hello, runoob ! hello, runoob !
    hello, runoob ! hello, ${your_name} !
  2. 获取字符串长度

    1
    2
    string="abcd"
    echo ${#string} #输出 4
  3. 提取子字符串
    以下实例从字符串第 2 个字符开始截取 4 个字符:

    1
    2
    string="runoob is a great site"
    echo ${string:1:4} # 输出 unoo
  4. 查找子字符串
    查找字符 i 或 o 的位置(哪个字母先出现就计算哪个):

    1
    2
    string="runoob is a great site"
    echo `expr index "$string" io` # 输出 4

注意: 以上脚本中 是反引号,而不是单引号' ,不要看错了哦。

Shell数组

bash支持一维数组(不支持多维数组),并且没有限定数组的大小。

类似于 C 语言,数组元素的下标由 0 开始编号。获取数组中的元素要利用下标,下标可以是整数或算术表达式,其值应大于或等于 0。

定义数组
在 Shell 中,用括号来表示数组,数组元素用”空格”符号分割开。定义数组的一般形式为:

1
数组名=(值1 值2 ... 值n)

例如:

1
array_name=(value0 value1 value2 value3)

或者

1
2
3
4
5
6
array_name=(
value0
value1
value2
value3
)

还可以单独定义数组的各个分量:

1
2
3
array_name[0]=value0
array_name[1]=value1
array_name[n]=valuen

可以不使用连续的下标,而且下标的范围没有限制。

读取数组
读取数组元素值的一般格式是:

1
${数组名[下标]}

例如

1
valuen=${array_name[n]}

使用 @ 符号可以获取数组中的所有元素,例如:

1
echo ${array_name[@]}

获取数组的长度
获取数组长度的方法与获取字符串长度的方法相同,例如:

1
2
3
4
5
6
# 取得数组元素的个数
length=${#array_name[@]}
# 或者
length=${#array_name[*]}
# 取得数组单个元素的长度
lengthn=${#array_name[n]}

简单的echo输出

echo的任务就是产生输出,可用来提示用户,或是用来产生数据供进一步处理

1
2
$ echo -n "Enter your name: " #显示提示
Enter your name: _ #键入数据

echo的转义序列

1
2
3
4
5
6
7
8
9
10
\a  警示字符
\b 退格
\c 输出中忽略最后的换行字符/
\f 清除屏幕
\n 换行
\r 回车
\t 水平制表符
\v 垂直制表符
\\ 反斜杠字符
\0ddd 将字符表示成1到3位的八进制数值

基本的I/O重定向

标准输入/输出可能是软件设计原则里最重要的概念.
这个概念就是:程序应该有数据的来源端、数据的目的端(数据要去的地方)以及报告问题的地方,它们分别称为标准输入、标准输出以及标准错误输出.
许多UNIX程序都遵循这一设计原则.默认的情况,它们会读取标准输入、写入标准输出,并将错误信息传递到标准错误输出.这类程序常叫做过滤器.

1
2
3
4
5
$cat
now is the time
for all good men
to come to the aid of their country
^D #Ctrl-D,文件结尾

谁替执行中的程序初始化标准输入、输出及错误输出?
答案是在登陆时,UNIX便将默认的标准输出、输出及错误输出安排成你的终端.I/O重定向就是你通过与终端交互,或是在Shell脚本里设置,重新安排从哪里输入或输出到哪里.

重定向与管道

  1. 以<改变标准输入
    program < file 可将program的标准输入修改为file:
    tr -d ‘\r’ < dos-file.txt …
  2. 以>改变标准输出
    program > file 可将program的标准输出修改为file:
    tr -d ‘\r’ < dos-file.txt > UNIX-file.txt
    这条命令会先以tr将dos-file.txt里的ASCII carriage-return(回车)删除,再将转换完成的数据输出到UNIX-file.txt.dos-file.txt里的原始数据不会有变化
    另外,>重定向符在目的文件不存在时,会新建一个.然而,如果目的文件已存在,他就会被覆盖掉;原本的数据都会丢失.
  3. 以>>附加到文件
    program >> file可将program的标准输出附加到file的结尾.
    如同>,如果目的文件不存在,>>重定向符便会新建一个.然而,如果目的文件存在,他不会直接覆盖掉文件,而是将程序所产生的数据附加到文件结尾处
  4. 以|建立管道
    program1 | program2可将program1的标准输出修改为program2的标准输入.
    虽然<与>可将输入与输出连接到文件,不过管道可以把两个以上执行中的程序衔接在一起。

    Shell 传递参数

    我们可以在执行 Shell 脚本时,向脚本传递参数,脚本内获取参数的格式为:$n。n 代表一个数字,1 为执行脚本的第一个参数,2 为执行脚本的第二个参数,以此类推……
    实例
    以下实例我们向脚本传递三个参数,并分别输出,其中 $0 为执行的文件名:
1
2
3
4
5
6
#!/bin/bash
echo "Shell 传递参数实例!";
echo "执行的文件名:$0";
echo "第一个参数为:$1";
echo "第二个参数为:$2";
echo "第三个参数为:$3";

为脚本设置可执行权限,并执行脚本,输出结果如下所示:

1
2
3
4
5
6
7
$ chmod +x test.sh
$ ./test.sh 1 2 3
Shell 传递参数实例!
执行的文件名:./test.sh
第一个参数为:1
第二个参数为:2
第三个参数为:3

另外,还有几个特殊字符用来处理参数:
$# 传递到脚本的参数个数
$* 以一个单字符串显示所有向脚本传递的参数

$$ 脚本运行的当前进程ID号
$! 后台运行的最后一个进程的ID号
$@ 与$* 相同,但是使用时加引号,并在引号中返回每个参数.
$- 显示Shell使用的当前选项
$? 显示最后命令的退出状态.

1
2
3
4
5
6
#!/bin/bash
echo "Shell 传递参数实例!";
echo "第一个参数为:$1";

echo "参数个数为:$#";
echo "传递的参数作为一个字符串显示:$*";

执行脚本,输出结果如下所示:

1
2
3
4
5
6
$ chmod +x test.sh
$ ./test.sh 1 2 3
Shell 传递参数实例!
第一个参数为:1
参数个数为:3
传递的参数作为一个字符串显示:1 2 3

$* 与 $@ 区别:

  • 相同点:都是引用所有参数。
  • 不同点:只有在双引号中体现出来。假设在脚本运行时写了三个参数 1、2、3,,则 “ * “ 等价于 “1 2 3”(传递了一个参数),而 “@” 等价于 “1” “2” “3”(传递了三个参数)。
1
2
3
4
5
6
7
8
9
10
#!/bin/bash
echo "-- \$* 演示 ---"
for i in "$*"; do
echo $i
done

echo "-- \$@ 演示 ---"
for i in "$@"; do
echo $i
done

执行脚本,输出结果如下所示:

1
2
3
4
5
6
7
8
$ chmod +x test.sh
$ ./test.sh 1 2 3
-- $* 演示 ---
1 2 3
-- $@ 演示 ---
1
2
3

简单的执行跟踪

程序时人写的,难免会出错.想知道你的程序正在做什么,有个好方法,就是把执行跟踪的功能打开.这会使得Shell显示每个被执行到的命令,并在前面加上”+”:一个加号后面跟着一个空格.

1
2
3
$sh -x nusers
+ who #被跟踪的命令
+ wc -l

-------------本文结束感谢您的阅读-------------