Shell编程极简入门实践
By StevenSLXie (Last updated: 29, Dec, 2014)
0. 写在前面
程序员多多少少都会和命令行打交道,一些常用的命令,比如-l -n -a等等),即使从网上找到了可能符合自己要求的代码,也往往因为看不懂而无法修改化为己用。
这个极简教程,或者说笔记,针对的是正是这部分读者。具体地说,通过学习这篇文档,你将获得以下技能:
- 熟练掌握Unix/Linux下的最常用命令及其最常见用法;
- 能够编写脚本,对文件进行批处理,对一些网络任务进行自动化等等;
- 避免写脚本过程中的最常见错误;
- (Hopefully)可以借此消除对命令行的恐惧;
这个教程的特点是:
- 不求全面,只求实用。只覆盖最常用的命令及其用法;
- 以大量例子为导向;
- 一边阅读一边动手写例程的话,大约只需要1.5-2.5小时的时间;
这篇文档假定你是在Linux/Unix环境下,比如Ubuntu, 比如Mac OS X。同时假定你至少了解一门其它的编程语言。这个教程的代码均在Mac OS下测试过,由于各种shell的标准差别很小,(有充足的理由相信)在别的平台应该也都能顺利运行。
1. Hello World
首先打开你用得最顺手的文本编辑器,在第一、二行分别打入
|
|
保存文件,文件可保存在你喜欢的文件夹,扩展名选择tutorial.sh。
接着,打开命令行工具Terminal,首先将工作目录改到你保存文件的文件夹,比如如果你将/Users/Steven/code,则在命令行里执行以下操作
|
|
tutorial.sh这个脚本,所以我们要先将工作目录转到这个脚本对应的文件夹下面。接着,在命令行继续输入
|
|
tutorial.sh变为一个可执行的文件。
接下来,我们就可以运行tutorial.sh这个脚本了。在命令行里打入
|
|
如无意外,你将看到命令行里返回./,系统会只在系统目录里面查找(准确来说是PATH变量定义的路径)。
我们回头来看看tutorial.sh里面的程序,目前它只有两行:
|
|
print是类似的。
就这样,我们完成了一个最简单的bash scripting的程序编写。这里面有几点需要注意:
- 执行脚本文件前,先要cd到文件所在的目录;
- 执行脚本文件前,先要chmod +x tutorial.sh将其变为可执行程序;
- 脚本文件的第一行,记得写上#!/bin/bash。
2. 整数和字符串
变量的定义很简单,按照以下格式就可以了:
|
|
比如定义一个字符串:
|
|
比如定义一个整型变量:
|
|
这里有几点要注意,一是变量的名字,虽然大小写不限,但按照惯例一般采用全大写的方式。第二点特别重要,让我们做一个小实验来说明一下。打开刚才的那个tutorial.sh文件,将之前的内容清空,并打入
|
|
如果你是直接复制以上的代码段,那么命令行应该会出现以下错误信息:
|
|
出现这个错误,是因为:定义变量时,=的前面和后面,都是不能有空格的!这一点可能和其它语言不一样,但请务必注意。因为出现这类错误时,报错信息定位的栏数(line 2),是指向你引用变量的那一段代码,而不是定义变量的那一行,因此debug起来可能不是那么直观。
于是,我们把代码改为:
|
|
命令行将显示Steven。
从上面的例子我们也可以看到,当你定义了一个变量,要引用它时,要在前面加上[1]:
|
|
可想而知,如果没有花括号,NAME和后面的SLXie就无法区分了。
对于字符串变量,既可以用单括号,比如"My name is ${NAME}SLXie."。请试着将它的双括号改为单括号,并观察它的输出结果。
单括号会将被引用字符串中的几乎所有特殊字符当作普通字符处理,比如上面的$,单括号时只把它当作一个普通的美元符号输出。
再看一个例子,试着在脚本分别输入这两行,并观察它们的输出。
|
|
在这里插播一句,看这个教程的时候,最好是看到那里,就动手写到哪里。写的时候,不要直接复制粘贴,而是试着手打代码到编辑器里面。有时候代码里有一些微小而琐碎的东西,一定要自己打一遍才能记得牢。比如,一开始可能容易将#!bin/bash。
花括号{}也可以用来对字符串进行某些操作。比如下面这个例子:
|
|
它会输出:
My name is StevenSLXie. People usually call me Steven.
这时候,${#USERNAME}则是获取字符串的长度。更多的字符串用法,我们将在后面的正则表达式哪一节看到。
3. 数组
数组可以这样简单粗暴地定义:
|
|
当数组体量太大时,这样定义未免麻烦,因此我们也可以用一行声明的方式来定义:
|
|
用declare -a来声明,后面一次性定义所有数组元素。请注意,在这里,整个数组用小括号括起来,而每个数组元素之间,是用空格来隔开的,而不是逗号或者其它。
访问数组的其中一个元素,和其它语言没什么不同。在你声明好数组之后,就可以访问数组元素了:
|
|
${#NAMES[0]}。
一个数组声明并定义后,我们仍可以二次定义它,比如下面的代码是在原来的数组基础上再添加一个人名。
|
|
命令行将返回:
|
|
4. 运算符
4.1 算术运算符
Shell编程里的算术运算符和大多数编程语言很类似,主要是这些+ - * / %等。如果你试着在命令行里执行运算的话,比如输入以下算式:
|
|
会得到:
|
|
这条错误信息。这是因为命令行的逻辑是它会把一行命令的第一个词当作是命令,在系统中寻找与之匹配的执行语句,因为在这里它会认为2是一个命令,而显然它不可能找到这个命令。要想执行运算,我们在命令行里打入
|
|
输出结果是expr是一个常用命令,evaluate an expression的意思。注意,这里数字和运算符之间,必须有一个空格。不然的话,如果你输入,
|
|
则会输出
|
|
这种情况下,2+2当成一个字符串,而evaluate一个字符串的结果,自然就是它本身了。算术运算当然也可以用变量,比如:
|
|
其它的算术运算符大体类似,但有一个要特别注意,如果你进行乘法运算,比如:
|
|
会输出:
|
|
这是因为。就像这样:
|
|
4.2 关系判断运算符
Shell提供了丰富的关系判断运算符,先来看一个例子,在tutorial.sh加入以下代码:
|
|
这是一个[$A -eq $B]是会报错的。(不如自己写段代码试一试?)
完整的关系判断运算符文档可以看这里:Unix Basic Operator
4.3 逻辑运算符
与主流编程语言用-a来表示的。看看下面这个例子:
|
|
这是判断变量A是否小于8且变量B是否大于B。
完整的关系判断运算符文档可以看这里:Unix Basic Operator
5. if条件判断、while循环、for循环
if | while | for语句和其它主流语言很相似,因此用起来应该不是大问题。值得注意的可能是以下几个小点:
- if语句的格式是if…then…elif…fi。注意,这里用fi来标记一个条件判断的结束。嗯,感觉是一种很调皮的设定。
- 所有的while和for语句,其执行的语句都都始于do,终于done。
- for的格式是for VAR in ARRAY,是Python的样式,和经典的C for循环可能稍有不同。
下面我们用一个例子来感受一下这三种语句。
|
|
第二行的ANS这个变量。
接着,我们用一个A大于等于10的时候,退出循环。
最后是一个B里面的元素依次打印出来。
这里要补充一点,在对SUM=expr 5 + 25
。
1的左边。
还有一点要注意,当我们对一个已经定义过的变量进行重新赋值的时候,是不需要加$。
6. 函数
Shell脚本里当然可以定义函数。比如这样的:
|
|
函数可以直接调用,比如下面这个脚本:
|
|
注意,但函数没有参数时,调用只需要写上函数名,而不是hello()之类的。
Shell函数的特殊之处,在于它参数传递的形式,具体地说,参数并不是像别的语言一样,写在括号里面,而是类似下面这个例子:
|
|
Shell用intro函数,则是这个样子:
|
|
命令行的输出则是:
|
|
参数不是写在括号里,而是在函数名之后依次排列,并以空格隔开。
函数也可以有返回值,比如:
|
|
返回变量A=hello,而是:
|
|
hello的返回值。
7. sed和正则表达式
正则表达式是一种特殊的字符串,用来描述一串具有某种共同特征的字符串。在进行批处理的时候,正则表达式有着异常强大的应用。
sed则是一个流编辑器(stream editor),它读入一个输出,并通过加工处理,输出经处理后的 文件/字符串 输出。下面我们通过一系列例子,来掌握sed的基本应用。
首先我们要来新建一些txt文件供sed处理。在命令行输入:
|
|
第一行tutorial.sh所在的目录里。
然后,分别打开那三个txt文件,将以下几行字符串拷贝到文件里。
|
|
接着,打开我们的#!/usr/bash。在里面输入:
|
|
保存后运行test-1.txt,如无意外的话,你会看到文档变成这个样子:
|
|
所有的空行被删除了。我们来看看test-1.txt,逐行扫描,找到空行,删除掉空行。
而txt的。但最好扩展名不要和文件夹的已有文件重复。
接着你可以试试将上面的sed语句改为:
|
|
sed -e ’s/But/but/’ test-2.txt
|
|
This is file with several lines
some of which are blank lines
for example, the line that follows is blank
but this line has several characters.
And this marks the end of the file.
|
|
1.This is file with several lines
2.some of which are blank lines
for example, the line that follows is blank
7.But this line has several characters.
And this marks the end of the file.
|
|
sed -i ‘.tmp’ ’s/^[ 1-3]//’ test-2.txt
|
|
.This is file with several lines
.some of which are blank lines
for example, the line that follows is blank
7.But this line has several characters.
And this marks the end of the file.
|
|
sed -i ‘.tmp’ ’s/[!.]$/;/’ test-2.txt
|
|
sed -i ‘.tmp’ ’s/^[. 1-9]*//;s/[;.!]$/ ENDING/’ test-2.txt
|
|
This is file with several lines
some of which are blank lines
for example, the line that follows is blank
But this line has several characters ENDING
And this marks the end of the file ENDING
|
|
sed -i ‘.tmp’ ’s/^some.*//’ test-2.txt
|
|
sed -i ‘.tmp’ ’s/….$//’ test-2.txt
|
|
sed -i ‘.tmp’ ’s/But/but/g’ test-2.txt
|
|
sed -i ‘.tmp’ ‘3 a
just some random text’ test-2.txt
|
|
sed -i ‘.tmp’ ‘$ a
just some random text’ test-2.txt
|
|
sed -i ‘.tmp’ ‘/text/ i
INSERT THIS BEFORE EVERY LINE CONTAINING TEXT’ test-2.txt
|
|
cd files
ls
|
|
正常的话会输出:
|
|
这两行包含着file。
grep可以同时搜索多个文件,比如这样:
|
|
test-1.txt:This is file with several lines
test-1.txt:And this marks the end of the file.
test-2.txt:This is file with several li
test-2.txt:And this marks the end of the file END
|
|
有时候你只想知道哪些文件包含了某个字符串,而对那一行的具体内容是什么并不重要,那么可以这样:
系统会打印出包含file,那么可以:
|
|
global regular expression print。所以“你问我支持不支持,他的名字叫全局正则表达式打印器,怎么能不支持?”(请忽略一个蛤丝的老梗~)。
我们来看几个例子:
输出结尾为END的那些行。
|
|
grep ‘characters.’ test-*.txt
|
|
#!/bin/bash
mkdir final
cd final
declare -a NAME
NAME=(1 2 3 4 5 6 7 8 9 10)
创建新文件。
for I in ${NAME[*]}
do
touch final-test-${I}.txt
done
往文档写入。这里使用的是echo,通过>
改变其默认输出。不妨思考一下如果用sed来实现会有什么问题?
for F in final-test-*.txt
do
echo ‘This is a test file…!!’ > $F
done
文件名首字母大写。注意echo和sed的连用,以及我们引用命令的一种新方法$(command)。还有mv这个新命令。
for F in final-test-*.txt
do
NEW=$(echo “$F” | sed -e ’s/^./F/’)
mv “$F” “$NEW”
done
删除标点。
for F in *.txt
do
sed -i.tmp ’s/…!!$//’ $F
done
大写。注意tr的使用。
for F in *.txt
do
tr ‘[:lower:]’ ‘[:upper:]’ < $F > FILE2
mv FILE2 $F
done
cd ../
mkdir repo
cd final
复制文件。
for F in *.txt
do
cp $F ../repo/$F
done
|
|
|
|
|
|
|
|