c语言中宏是一种预处理指令,它提供了一种机制,可以用来代替源码中的字符串。

mkdir,touch都是系统自带的程序,一般在/bin或者/usr/bin目录下。for,都, done是shell
脚本语言的关键字。

shell和shell脚本的概念

shell是指一种应用程序,这个应用程序提供了一个界面,用户通过这个界面访问操作啊系统内核的服务。
Ken Thompson的sh是第一种Unix Shell,Windows Explorer是一个典型的图形界面Shell。

shell脚本(shell script),是一种为shell编写的脚本程序。业界所说的shell通常都是指shell脚本,但
读者朋友要知道,shell和shell script是两个不同的概念。由于习惯的原因,简洁起见,本文出现的shell编程
都是指脚本编程,不是指开发shell自身。

环境
shell编程跟Java,PHP编程一样,只要有一个能编写代码的文本编辑器和一个能解释执行的脚本解释器就可以了。

OS
当前主流的操作系统都支持shell编程,本文档所述的shell编程是指Linux下的shell,讲的基本都是POSIX
标准下的功能,所以,也适用于Unix及BSD。

Mac OS
Mac OS不仅带了sh、bash这两个最基础的解释器,还内置了ksh、csh、zsh等不常用的解释器。

Windows上的模拟器

Windows出厂时没有内置shell解释器,需要自行安装,为了同时能用grep,awk,curl等工具,最好装一个
cygwin或者mingw来模拟Linux环境。

①、cygwin。
②mingw。

脚本解释器
sh
即Bourne shell,POSIX(Portable Operating System Interface)标准的shell解释器,它的二进制文件
路劲通常是/bin/sh,由Bell Labs开发。

本文讲的是sh,如果你使用其它语言用作shell编程,请自行参考相关语言的文档。

bash
Bash是Bourne shell的替代品,属GNU Project,二进制文件通常是/bin/bash。业界通常混用bash、sh和shell,
比如你会经常在招聘运维工程师的文档中见到:熟悉Linux Bash编程,精通Shell编程。

在CentOS里,/bin/sh是一个指向/bin/bash的符号链接:

但在Mac OS上不是,/bin/sh和/bin/bash是两个不同的文件,经管它们的大小只相差100字节左右:

高级编程语言
理论上讲,只要一门语言提供了解释器(而不仅是解释器),这门语言就可以胜任脚本编程,常见的解释型
语言都是可以用作脚本编程的,如:Perl,Tcl,Python,PHP,Ruby。Perl是最老牌的
脚本编程语言了,Python这些年也成了一些Linux发行版的预置解释器。

编译型语言,只要有解释器,也可以用作脚本编程,如C shell是内置的(/bin/csh),Java有第三方解释器
Jshell,Ada有收费的解释器AdaScript。

如何选择shell编程语言

熟悉vs陌生

如果你已经掌握了一门编程语言()

简单vs高级
如果你觉得你自己熟悉的语言(如Java C)写shell脚本是在太啰嗦,你只是想做一些备份文件、安装软件、
下载数据之类的事情,学者使用sh,bash会是一个好主意。

shell之定义了一个非常简单的编程语言,所以,如果你的脚本程度复杂度较高,或者要操作的数据结构比较复杂,
那么还是应用该使用Python‘Perl这样的脚本语言,或者是你本来就已经很擅长的搞基语言,因为sh和bash在这方面
很弱,比如说:
。它的函数智能返回字符串,无法返回数组
。它不支持面向对象,你无法实现一些优雅的设计模式
。它是解释型的,一遍解释一边执行,连PHP那种预编译都不是,如果你的脚本包含错误,只要没执行到这一行,就不会报错。

环境兼容性
如果你的脚本是提供给别的用户使用,使用sh或者bash,你的脚本将具有最好的环境兼容性,Perl很早就是Linux标配了。
Python这些年也成了一些Linux发行版的标配,至于Mac OS,它默认安装了Perl、Python、Ruby、PHP、Java等
主流编程语言。

第一个shell脚本

编写

打开文本编辑器,新建一个文件,扩展名为sh(sh代表shell),扩展名并不影响标本执行,见名知意就好,如果你用PHP
写shell,扩展名就用PHP就好了。

输入一些代码,第一行一般是这样:

!/bin/bash

!/usr/bin/php

!是一个约定的标记,它告诉系统这个脚本需要什么解释器来执行。

运行
运行Shell脚本有两种方法:
作为可执行程序

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告诉系统说,就在当前目录找。

通过这种方式运行bash脚本,第一行一定要写对,要让系统查找正确的解释器。

这里的“系统”其实就是shell这个应用程序(想象一下Windows Explorer),但我故意写成系统,是方便理解,既然这个系统
就是指shell,那么一个使用/bin/sh作为解释器的脚本是不是可以省去第一行的?是的。

作为解释器参数
这种运行方式是,直接运行解释器,其参数就是shell脚本的文件名,如:
/bin/sh test.sh
/bin/php test.php

这种方式运行的脚本,不需要再第一行指定解释器信息,写了也没用。

变量

定义变量

定义变量时,变量名不加美元符号($),如:

your_name='qinjx'
注意,变量名和等号之间不能有空格,这可能和你熟悉的所有编程语言都不一样。
除了显示地直接赋值,还可以用语句给变量赋值,如:

for file in ls /etc

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

your_name='qinjx'
echo $your_name
echo ${your_name}

变量名外面的花括号是可选的,加不加都行,加花括号是为了帮助解释器识别变量的边界,比如下面这种情况:

for skill in Ada Coffe Action Java; do
echo "I am good at ${skill}Script"
done

如果不给skill变量加花括号,写成echo "I am good at $skillScript",解释器就会把$skillScript当成一个
变量(其值为空),代码执行结果就不是我们期望的样子了。

推荐给所有变量加上花括号,这是个好的编程习惯。Intellij IDEA编写shell script时,IDE就会提示加花括号。

重定义变量
已定义的变量,可以被重新定义,如:
your_name='qinjx'
echo $your_name

your_name='alibaba'
echo $your_name

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

注释:
以#开头的行就是注释,会被解释器忽略。
多行注释

sh里没有多行注释,只能每一行加一个#号。就像这样:

如果在开发过程中,遇到大段的代码需要临时注释起来,过一会儿又取消注释,怎么办呢?每一行加一个#符号太费劲了,
可以把这一段要注释的代码用一对或括号括起来,定义成一个函数,没有地方条用这个函数,这块代码就不会执行,达到了和注释
一样的效果。

字符串

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

单引号

str='thiis is a string'

单引号字符串的限制:
①、单引号里的任何字符都会原样输出,单引号字符串中的变量是无效的。
②、双引号字符串汇总不能出现单引号(对单引号使用转义符后也不行)

双引号
your_name='qingjx'
str="hello,I know your are "$yout_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

条件判断
流程控制

和Java、PHP等语言不一样,sh的流程控制不可为空,如:

expr > bc (()) 和let是bash内建命令,执行效率高;而expr和bc是系统命令,会消耗内存,执行效率低。 (()) 、let和expr只支持整数运算,不支持浮点运算;而bc支持浮点运算。 #数值i=3*(5+2) (方法一:$(())实现) val=$((3*(5+2))) echo "val=$val"; #数值i=3*(5+2) (方法二:let实现) let "val=3*(5+2)" echo "val=$val" #数值i=3*(5+2) (方法三:expr实现) val=`expr 3 \* \( 5 + 2 \)` echo "val=$val"; #数值i=3*(5+2) (方法四:bc实现) val=`echo "3*(5+2)"|bc` echo "val=$val"; 应用实例二:分别用上面四种方式实现"数值+1"。 #!/bin/bash val=0; #数值加1 (方法一) ((val++)); echo "val=$val"; val=0; #数值加1(方法二) let val++ echo "val=$val"; val=0; #数值加1(方法三) val=`expr $val + 1` echo "val=$val" val=0; #数值加1(方法四) val=`echo "$val+1"|bc`; echo "val=$val"; exit 0; 第七部分 字符运算 1、字符串定义 字符串说明 字符串定义 定义说明 ${var} 变量var的值,和$var相同 第八部分 bash自带参数 bash 自带参数 参数 参数说明 $? 上一个代码或者shell程序在shell中退出的情况,如果正常退出则返回0;反之为非0值。 $# 代表后接的参数【个数】 $@ 代表全部变量,如:【$1, $2, $3, $4】之意,每个变量是独立的(用双引号括起来) $- 在Shell启动或者使用set命令时提供选项 $$ 当前shell的进程号 $! 上一个子进程号 $() 当前shell名称 $n 位置参数值,n标识位置,n的取值为大于0的整数。例如,$1标识第一个参数。 第九部分 bash调试 1 bash调试 bash [-nvx] scripts.sh 选项和参数: -n:不要执行script,仅查询语法问题; -v:在执行script前,先将scripts的内容输出到屏幕上; -x:将使用到的script内容显示到屏幕上,这是很有用的参数; 例如:想要执行的bash脚本并查看bash的调用流程,可以通过以下命令: bash -x test.sh 2、echo 调式 echo [OPTION]STRING -n:输出内容之后,不换行。默认是输入内容之后,换行。 -e:开启反斜线\转义功能。 -E:开启反斜线\转义功能(默认)。 例如:输出"please input a number:"之后不换行。 echo -n "please input a number:" 3、printf 和echo一样,printf也能用于输出。语法格式和C语言中printf一样。 例如:输出“hello printf”之后换行。 第十部分 bash注释 1、单行注释 #echo "single line" 说明:#放在文件开头,表示注释掉本行。 第十一部分 bash内建指令 1、内建命令查看方法 基本格式 type cmd 格式说明 type是命令关键字,cmd表示查看的命令;若输出builtin,则该命令是bash额内建命令。 例如: type echo 除此之外,用户也可以通过man bash 或者man builtins查看bash的全部内置命令。 2、常用内建命令说明 ①、echo 命令:echo arg 功能:在屏幕上显示出由arg指定的字串 ②、read 命令格式:read变量名表 功能:从标准输入设备读入一行,分解成若干字,赋值给bash程序内部定义的变量。 ③、shift 命令:shift[N](N为大于0的整数;当N省略时,等价与于"shift 1") 功能:所有的参数依次向左移动N个位置,并使用$#减少N,直到$#=0为止。 Shell中的变量 这里需要注意的就是,"="前后不能有空格,命令规则就和其它语言一样了。 当想要访问变量的时候,需要使用$,否则输出的将是纯文本内容 Shell中的四则运算 运算符 含义 + 加法运算 - 减法运算 * 乘法运算 / 除法运算 % 求余 == 相等 = 赋值 != 不相等 ! 非 -o 或 -a 与 关系运算符 运算符 含义 -eq 两个数相等返回true -ne 两个数不相等返回true -gt 左侧数大于右侧数返回true -lt 左侧数小于右侧数返回true -ge 左侧数大于等于右侧数返回true -le 左侧数小于等于右侧数返回true 字符串运算符 运算符 含义 = 两个字符串相等返回true != 两个字符串不相等返回true -z 字符串长度为0返回true -n 字符串长度不为0返回true 运算符 含义 -d file 检测文件是否是目录,如果是,则返回true -r file 检测文件是否可读,如果是,则返回true -w file 检测文件是否可写,如果是,则返回true -x file 检测文件是否可执行,如果是,则返回true -s file 检测文件是否为空(文件大小是否大于0,不为空返回true) -e file 检测文件(包括目录)是否存在于,如果是,则返回true 6、数组: arr=(1 2 4 5) #定义数组 arr2=(aa bb cc dd ee) #定义数组 val=${arr[3]} #找到摸一个下标的数,然后赋值 echo $val; #打印 val2=${arr2[3]} #找到某一个下标的数,然后赋值 echo $val2; #打印 length=${#arr[*]} @获取数组长度 echo $length; printf 同C语言了 test命令 参数 含义 -e file 文件存在则返回真 -r file 文件存在并且可读则返回真 -w file 文件存在并且可写则返回真 -x file 文件存在并且可执行则返回真 -s file 文件存在并且内容不为空则返回真 -d file 文件目录存在则返回真 Shell是什么 shell是指一种应用程序,这个应用程序提供了一个界面,用户通过这个界面访问操作系统内核的服务,Shell脚本(shell script)。 是一种为shell编写的脚本程序。我们经常说的shell通常都是指shell脚本。 环境和工具 第一个shell脚本 我们先来看一个例子 我相信写过代码的同学,应该对下面的代码很书序并不陌生,(假设文件名叫op_base.sh): #!/bin/bash mkdir code cd code for ((i=0;i<3;i++)); do touch test_${i}.txt echo "shell很简单" >> test_${i}.txt done; 第一行:从系统path中寻找指定脚本的解释器程序 第二行:创建名叫code文件夹 第三行:进入创建的文件夹 第四行:for循环3次 第四行:创建文件 第五行:往创建的文件中写入信息 第六行:结束循环 1.1.4文本处理命令 sort:文本排序 uniq:文本去重 tr:替换命令 grep:查找字符串 diff:文件对比,找出文件差异 解释:history命令列出用户的历史使用命令,然后通过awk过滤出第二列,因为第一列是文件的行数标号, 接着进行一个sort排序,所有相同的命令都放在一起方便接下来的uniq -c命令去重并且统计出每个命令出现的次数, 再然后是sort -k1nr -n选项的意思是按照数字的大小即uniq统计出的次数进行排序-r则是反向的意思,即本来是 正序变为逆序;然后取其前十,即最常用的前是个命令,最后用一下tr命令将小写字母替换为大写字母。 hostname:查看主机名 w,who:列出系统登录的用户 uptime:查看系统运行时间 uname:查看系统信息 date:显示和设置系统日期和时间 id:显示用户属性 1.1.6 shell命令进阶 paster:合并文本 dd:备份和拷贝文件(和vim和剪切命令一样) tar:打包和解包文件 mount,umount:挂载和卸载存储介质 df:报告文件系统磁盘空间利用率 du:评价文件空间利用率 ps:查看系统的进程 pidof:列出进程的pid top:相当于Linux的任务管理器 &:将作业后台运行 jobs:查看作业 bg:让挂起的进程在后台继续执行 fg:将后台进程放入前后 fdisk:查看系统的磁盘信息 部分演示: df -h 解释:先执行sleep &让这个进程在后台执行,然后我们先使用ps配合grep命令查看一下sleep进程的信息, 接着jobs查看一下后台运行的作业,使用fg命令使其继续在前台执行,我们可以看到显示了sleep 100, 接着我们又不想让他继续执行了,使用Ctrl+z使其挂起,我们用jobs查看,可以看出其处于stopped状态,接着 使用bg命令使其继续在后台执行,查看状态,处于running状态; 至此,shell的基本命令回顾完毕,这些都是脚本中常用的命令; shell编程基础 #!/bin/bash echo "hello,world"; 分析:还是hello world起步,第一行的#!叫做Shebang 什么是键值商店? 键值存储的本真是能够在键内存储一些称为为值得数据。只有当我们知道存储的特定秘钥时才可以检索 时才可以检索该值。没有直接的方法来按值搜索秘钥。从某种意义上说,它就像一个非常大的哈希/字典, 但它是持久的,即当你的程序结束时,数据不会消失。因此,例如,我可以使用set将值栏存储在键foo中。 set foo bar Redis永久存储数据,所以如果我后来问“关键foo中存储的值是多少?”Redis会回复条形码。 get foo => bar 键值存储提供的其他常见操作时DEL,用于删除给定键以及其关联值,set-if-not-exists(在 Redis上称为SETNX),仅在键尚未存储在时才为键分配值存才和INCR,以原子方式递增存储在给定键 中的数字。 set set foo 10 INCR foo => 11 INCR foo => 12 INCR foo => 13 原子操作 INCR有一些特别之处。你可能想知道为什么Redis会提供这样的操作,如果我们可以使用一些代码完成它? 毕竟,它很简单。 x = get foo x = x + 1 set foo x 问题是,只要有一个客户端同时使用秘钥foo,以这种方式递增就会起作用。看看如果两个客户端同时访问此秘钥 会发生什么。 x = get foo (yields 10) y = get foo (yiedls 10) x = x + 1 (x is now 11) y= y + 1 (y is now 11) set foo x (foo is now 11) set foo y (foo is now 11) 出了点问题!我们将值递增两次,但不是从10到12,我们的键保持11,这是因为完成的增量get / increment / set 不是原子操作。相反,Redis,memcached,...提供的INCR是原子实现,服务器将在完成增量所需 的时间内保存秘钥,以防止同时访问。 Redis与其他键值存储的不同之处在于它提供了类似于INCR的其他操作,可用于模拟复杂问题。这就是为什么 你可以使用Redis编写整个web应用程序而不使用像SQL数据库这样的其他数据库,而且不会发疯。 超越键值存储:列表 在本节中,我们将了解构建Twitter克隆所需的Redis功能。首先要知道的是Redis值不仅仅是字符串。Redis支持lists, set,hash,sort set,bitmaps和hyperloglog类型作为值,并且有对它进行操作的原子操作,因此即使 对同一个键进行多次访问也是安全的。 让我们从列表开始。 LPUSH mylist a (now mylist holds 'a') lpush mylist b (now mylist holds 'b', 'a') lpush mylist c (now mylist holds 'c', 'b','a') lpush表示左推,即向存储在mylist中的列表的左侧(或者头部)添加元素。如果mylist不存在,则在 push操作之前自动创建为空列表。可以想象,还有一个rpush操作,它将元素添加到列表的右侧(尾部)。 这对我们的Twitter克隆非常有用。username:updates例如,可以将用户更新添加到存储在其中的 列表中。 当然,有一些操作可以从列表中获取数据。例如,lrange返回列表或者整个列表中的范围。 lrange mylist 0 1 => c,b lrange使用从零开始的索引-这是第一个元素是0,第二个元素是1,以此类推。命令参数是 lrange key first-index last-index。在最后的索引参数可以是负数,具有特殊的意义; -1是列表,-2倒数第二的最后一个元素,依次类推。因此,要使用整个列表; lrange mylist 0 -1 => c,b,a 其他重要的操作时llen,它返回列表中元素的数量,ltrim就像lrange但不是返回指定的范围修建列表, 所以它就像从mylist获取范围,将此范围设置为新值但是这样做原子。 设置数据类型 目前我们在本教程中不使用set类型,但由于我们使用排序集,这是一种更强大的集合版本,最好开始引入集合 (这是一个非常有用的数据结构本身),以及后来的排序集。 有更多数据类型而不仅仅是列表。Redis还支持集合,集合是未分类的元素集合。可以添加,删除和测试成员 的存在,并执行不同集合之间的交集。当然可以获取set的元素。一些例子会更清楚。请记住是设置附加的操作, srem是从集合中去除操作,sismember是测试,如果构建的操作,及烧结矿执行交叉点的操作。其他操作时 scard以获得set的基数(元素数),和smembers返回set的所有成员。 sadd myset a sadd myset b sadd myset foo sadd myset bar sadd myset 4 smembers myset bar,a,foo,b 请注意,smembers不会以我们天剑它们的相同顺序返回元素,因为集合是未排序的元素集合。 如果要按顺序存储,最好使用列表。针对集合的一些操作。 sadd mynewset b sadd mynewset foo sadd mynewset hello 排序集数据类型 排序集类似于集合;元素集合。但是,在“排序集”中,每个元素都与浮点值相关联,称为元素得分, 排序集内的元素是有序的,因为我们总是可以按分数比较两个元素(如果得分恰好相同,我们将这两个 元素作为字符串进行比较)。 和排序集中的集合类似,无法添加重复元素,每个元素都是唯一的。但是,可以更新元素的分数。 sorted set命令带有前缀z。以下是排序集用法的示例。 zadd zset 10 a zadd zset 5 b zadd zset 12.55 c zrange zset 0 -1 => b,a,c 在上面的示例中,我们添加了一些元件和zadd,后来检索和元件zrange。如您所见,元素根据其分数 按顺序返回。为了检查给定元素是否存在,并检查其得分(如果存在),我们使用zscore命令。 zscore zset a => 10 zscore zset non-existing-element => null 排序集是一种非常强大的数据结构,您可以按分数范围,按字典顺序,以相反顺序查找元素,等等。 这是我们在程序中使用的最后一个数据结构,并且非常容易喘气,因为几乎每种编程语言都有相同的机构,哈希。 Redis hashes基本上类似于Ruby或者Python哈希,一组和值相关联的字段。 哈希是标识对象的理想数据结构。例如,我们使用hashes来表示Twitter克隆中的用户和更新。 好的,我们刚刚公开了Redis主要数据结构的基础知识,我们已经准备好开始编码了。 先决条件 数据布局 使用关系数据库时,必须设计数据库模式,以便我们知道数据库将包含的表,索引等。我们在Redis中没有表格, 那么我们需要设计什么?我们需要确定标识对象所需的键以及此键需要保存的值。 让我们从用户开始吧。当然,我们要用他们的用户名,用户ID,密码,跟随给定用户的用户集,给定用户遵循 的用户集等来代表用户。第一个问题是,我们应该如何识别用户?和关系数据库一样,一个好的解决方案 是识别具有不同数字的不同用户,因此我们可以将唯一ID和每个用户相关联。对此用户的每个其它引用 都将由ID完成。使用我们的原子INCR操作创建唯一ID非常简单。当我们创建一个新用户时,我们可以做这 样的事情,假设用户被称为“antriez”。 INCR next_user_id => 1000 hmset user:1000 username antirez password p1pp0 注意:你应该在实际应用程序中使用散列密码,为简单起见,我们将密码以明文形式存储。 我们使用next_user_id秘钥以便始终未每个新用户获取唯一ID。然后我们使用这个唯一的ID来 命名持有hash的秘钥和用户名。例如,有可能够从用户名获取用户ID非常有用,因此每次添加用户时,我们 还会填充users秘钥,即秘钥,用户名为字段,ID为值。 hset users antires 1000 这可能一开始看起来很奇怪,但请记住,我们只能以直接的方式访问数据,而不需要二级索引。 无法告诉Redis返回包含特定值得键。 这也是我们的力量。这种新的范例迫使我们组织数据, 以便所有内容可以通过主键访问,并以关系数据库术语说话。 追随者,关注和更新 我们的系统还有另一个核心需求。用户可能有跟随他们的用户,我们称之为他们的关注着。 用户可能会关注其他用户,我们称之为以下用户。我们有一个完美的数据结构。那是......集。 集合元素的唯一性以及我们可以在恒定时间内测试的实时是两个有趣的特征。然而,还要记住给定用户开始关注 另一个的时间呢?在我们简单的Twitter克隆的增强版本中,这可能很有用,