Linux Shell脚本

简介

  • Linux的默认Shell是Bash,可以与内核交互,是学习Linux的必备知识
  • Bash:Bourne Again Shell(/bin/bash)

Shell概念

  • Shell 既是一个连接用户和 Linux 内核的程序,又是一门管理 Linux 系统的脚本语言,也就有了Shell编程(就像Python编程、Java编程)

    Shell程序连接了用户和 Linux 内核,让用户能够更加高效、安全、低成本地使用 Linux 内核,这就是 Shell 的本质

    当然,我们使用命令或者执行脚本还是在应用层,Shell程序(bash)负责解释我们的指令,即Shell本身是独立的一层,所有与内核的交互必须经过这里

    别把bash和Linux终端命令行终端搞混了!

    Shell针对的不是命令就是程序,都可以叫命令或程序

  • 不同于图形界面,Shell直接使用命令。但也是控制程序的运行,都需要查找程序在硬盘上的安装位置,然后将它们加载到内存运行;Load into Memory

  • 我们运行一个命令,大部分情况下 Shell 都会去调用内核暴露出来的接口,这就是在使用内核,只是这个过程被 Shell 隐藏了起来

    接口其实就是一个一个的函数,使用内核就是调用这些函数。这就是使用内核的全部内容了吗?嗯,是的!除了函数,你没有别的途径使用内核

    • 如图所示:通过 Shell 才能运行其它的应用程序(这个图可以记一下!)
    • 有的编程语言,如 C/C++、Pascal、Go语言、汇编等,必须在程序运行之前将所有代码都翻译成二进制形式,也就是生成可执行文件
      • 用户拿到的是最终生成的可执行文件,看不到源码
      • 这个过程叫做编译(Compile),这样的编程语言叫做编译型语言,完成编译过程的软件叫做编译器(Compiler)
    • 而有的编程语言,如 Shell、JavaScript、Python、PHP等,需要一边执行一边翻译,不会生成任何可执行文件
      • 用户拿到源码即可运行程序,程序运行后会即时翻译,翻译完一部分执行一部分,不用等到所有代码都翻译完
      • 这个过程叫做解释,这样的编程语言叫做解释型语言或者脚本语言(Script),完成解释过程的软件叫做解释器
    • 编译型语言的优点是执行速度快、对硬件要求低、保密性好,适合开发操作系统、大型应用程序、数据库等
    • 脚本语言的优点是使用灵活、部署容易、跨平台性好,非常适合 Web 开发以及小工具的制作。
    • Shell 就是一种解释型脚本语言
  • OK,现在明确一个概念即可

    • Shell的作用是解释我们要执行的命令或者程序给内核

    • 用户的任何操作都要通过Shell

    • Shell可以编写脚本程序(命令的集合)

      • 其他程序采用的语言不同,调用内核的方式也不同,一般都会用到Shell
      • 因此,Shell解释的可以是其他程序,也可以是自己的脚本程序
    • Shell本身也是一个程序,就像OS

      大家所说的 Shell 强大,并不是 Shell 本身功能丰富,而是它擅长使用和组织其他的程序
      Shell 就是一扇门,这正是 Shell 的魅力所在

Linux 运维工程师(OPS)

  • 由于 Linux 服务器的大规模应用,需要一批专业的人才去管理

    OPS 的主要工作就是搭建起运行环境,让程序员写的代码能够高效、稳定、安全地在服务器上运行,他们属于后勤部门。OPS 的要求并不比程序员低,优秀的 OPS 拥有架设服务器集群的能力,还会编程开发常用的工具

    • 安装操作系统,例如 CentOS、Ubuntu 等。
    • 部署代码运行环境,例如网站后台语言采用 PHP,就需要安装 Nginx、Apache、MySQL、PHP 运行时等。
    • 及时修复漏洞,防止服务器被攻击,这包括 Linux 本身漏洞以及各个软件的漏洞。
    • 根据项目需求升级软件,例如 PHP 7.0 在性能方面获得了重大突破,如果现在服务器压力比较大,就可以考虑将旧版的 PHP 5.x 升级到 PHP 7.0。
    • 监控服务器压力,别让服务器宕机。例如淘宝双十一的时候就会瞬间涌入大量用户,导致部分服务器宕机,网页没法访问,甚至连支付宝都不能使用。
    • 分析日志,及时发现代码或者环境的问题,通知相关人员修复
  • 服务器一旦多了,人力工作都需要自动化起来,跑一段代码就能在成千上万台服务器上完成相同的工作,例如服务的监控、代码快速部署、服务启动停止、数据备份、日志分析等

  • Shell 脚本是实现 Linux 系统自动管理以及自动化运维所必备的工具

    除了 Shell,能够用于 Linux 运维的脚本语言还有 Python 和 Perl,参考

    当然,Shell编程只是Shell解释器的“副业”

Shell基础

常见的Shell

  • 不同的组织机构开发了不同的 Shell,它们各有所长,有的占用资源少,有的支持高级编程功能,有的兼容性好,有的重视用户体验

    常见的 Shell 有 sh、bash、csh、tcsh、ash 等

    bash shell 是 Linux 的默认 shell,也是接下来要学习的

  • bash就是Shell解释程序的一种,就是那扇门!

    往后的日子我们说bash就代表Shell,bash这个程序放在/bin/bash

    现代的 Linux 上,sh 已经被 bash 代替,/bin/sh往往是指向/bin/bash的符号链接

    # 输出 SHELL 环境变量
    $ echo $SHELL
    /bin/bash
    

使用Shell

  • 现在的Linux大多都是直接进入图形界面,Shell客户端在这里被隐藏了,进入我们的终端就可以使用Shell

    也就是说终端就是Shell的客户端,脚本中的语法命令按理说都可以单行在这里执行!

  • 另一种方式是在启动时,CentOS会创建 6 个虚拟控制台,按下快捷键Ctrl + Alt + Fn(n=2,3,4,5,6)可以从图形界面模式切换到控制台模式,按下Ctrl + Alt + F1可以从控制台模式再切换回图形界面模式

    也就是说,1 号控制台被图形桌面程序占用了

Shell命令格式

  • Shell命令分为两种,Shell 自带的命令称为内置命令,更多的命令是外部的应用程序

    command [选项] [参数]
    
  • Shell命令的本质是什么?

    Shell 内置命令的本质是一个自带的函数,执行内置命令就是调用这个自带的函数。因为函数代码在 Shell 启动时已经被加载到内存了,所以内置命令的执行速度很快。

    Shell 外部命令的本质是一个应用程序,执行外部命令就是启动一个新的应用程序。因为要创建新的进程并加载应用程序的代码,所以外部命令的执行速度很慢

    例如我们使用C语言编写一个累加的函数编译后放在~/bin目录,在终端执行[roy@localhost ~]$ getsum -s 1 -e 100就能得到结果;因为Shell会去$PATH指定的目录下寻找与getsum同名的程序并启动运行

    如果你想知道更多请

  • Shell命令的选项和参数在本质上到底是什么?

    参考

    不管是内置命令还是外部命令,它后面附带的数据最终都以参数的形式传递给了函数。实现一个命令的一项重要工作就是解析传递给函数的参数

  • 命令提示符$

    可以更改,输入命令时换行会出现>,也可以改

    参考

第一个脚本

  • 废话不多说写一个

    #!/bin/bash
    # Copyright (c) http://c.biancheng.net/shell/
    
    echo "What is your name?"
    read PERSON
    echo "Hello, $PERSON"
    

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

    echo 命令用于向标准输出文件(Standard Output,stdout,一般就是指显示器)输出文本

    read 命令用来从标准输入文件(Standard Input,stdin,一般就是指键盘)读取用户输入的数据(Linux一切皆文件记得吗?键盘也是文件目录)

    这个脚本叫test.sh,不加.sh也行,怎么都行!Shell解释的时候按人家的规则办,不行就给你报错!

  • 执行脚本

    [roy@localhost ~]$ cd demo                #切换到 test.sh 所在的目录
    [roy@localhost demo]$ chmod +x ./test.sh  #给脚本添加执行权限
    [roy@localhost demo]$ ./test.sh           #执行脚本文件
    Hello World !                             #运行结果
    

    当然,可以不写该死的#!这一行,直接在执行时指定

    只不过多了个bash找解释器的过程

    [roy@localhost demo]$ bash test.sh
    Hello World !
    
    # $是Shell特殊变量
    echo $$  #输出当前进程PID
    
  • source

    source 是Shell 内置命令的一种,它会读取脚本文件中的代码,并依次执行所有语句

    强制执行脚本文件中的全部命令,而忽略脚本文件的权限

    [roy@localhost demo]$ source ./test.sh  #使用source
    Hello World !
    
  • Shell的四种运行方式

    • 交互式的登录 Shell;
    • 交互式的非登录 Shell;
    • 非交互式的登录 Shell;
    • 非交互式的非登录 Shel

    参考

  • Shell配置文件的加载

    无论是否是交互式,是否是登录式,Bash Shell 在启动时总要配置其运行环境,例如初始化环境变量、设置命令提示符、指定系统命令路径等。这个过程是通过加载一系列配置文件完成的,这些配置文件其实就是 Shell 脚本文件

    如果是登录式的 Shell,首先会读取和执行 /etc/profiles,这是所有用户的全局配置文件,接着会到用户主目录中寻找 ~/.bash_profile~/.bash_login 或者 ~/.profile,它们都是用户个人的配置文件

  • 如何编写自己的Shell配置文件

    对于普通用户来说,也许 ~/.bashrc 才是最重要的文件,因为不管是否登录都会加载该文件

    如果我们想增加自己的路径,可以将该路径放在 ~/.bashrc 文件中,例如:

    vim ~/.bashrc
    # 将主目录下的 addon 目录也设置为系统路径
    PATH=$PATH:$HOME/addon
    

    规则是:各路径使用冒号分割

    将下面的代码添加到 ~/.bashrc 文件中,然后重新启动 Shell(终端),命令提示符就变成了[c.biancheng.net]$

    PS1="[c.biancheng.net]\$ "
    

Shell编程

  • 暂时把Shell是个程序的事抛开,反正最后全部所有的一切都需要这个程序解释!从这里开始是另一方面,把它当做一门编程语言,写出来的脚本还归这个程序解释!

变量

  • 在 Bash shell 中,每一个变量的值都是字符串,无论你给变量赋值时有没有使用引号,值都会以字符串的形式存储

    variable=value
    variable='value'
    variable="value"
    

    如果 value 包含了空白符,那么就必须使用引号包围起来

    以双引号" "包围变量的值时,输出时会先解析里面的变量和命令,单引号就是纯文本;其他没有特别要求的字符串等最好都加上双引号,详情了解

    注意,赋值号=的周围不能有空格,这可能和你熟悉的大部分编程语言都不一样

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

  • 将命令的执行结果赋值给变量(命令替换)

    variable=`command`	# 这是个反引号,容易混淆,不推荐这种方式
    variable=$(command)
    
    # 将日志内容赋值给变量log
    [roy@localhost demo]$ log=$(cat log.txt)
    
  • 只读变量,不允许修改,使用readonly命令即可

    #!/bin/bash
    
    myUrl="http://c.biancheng.net/shell/"
    readonly myUrl
    myUrl="http://c.biancheng.net/shell/"
    
  • 删除变量

    unset variable_name
    # unset 命令不能删除只读变量。
    
  • 作用域

    有的变量只能在函数内部使用,这叫做局部变量(local variable);
    有的变量可以在当前 Shell 进程中使用,这叫做全局变量(global variable);
    而有的变量还可以在子进程中使用,这叫做环境变量(environment variable)

  • 局部变量

    Shell 函数中定义的变量默认也是全局变量,它和在函数外部定义变量拥有一样的效果,这个特性和 JavaScript 中的变量是类似的

    要想变量的作用域仅限于函数内部,可以在定义时加上local命令

    #!/bin/bash
    
    #定义函数
    function func(){
        a=99
    }
    #调用函数
    func
    #输出函数内部的变量
    echo $a
    # 输出结果为99
    
  • 全局变量

    所谓全局变量,就是指变量在当前的整个 Shell 进程中都有效。每个 Shell 进程都有自己的作用域,彼此之间互不影响

    在一个 Shell 进程中可以使用 source 命令执行多个 Shell 脚本文件,此时全局变量在这些脚本文件中都有效(这就叫全局)

    怎么才能证明进程间相互独立呢?打开两个终端就行

  • 环境变量

    环境变量被创建时所处的 Shell 进程称为父进程,如果在父进程中再创建一个新的进程来执行 Shell 命令,那么这个新的进程被称作 Shell 子进程,它会继承父进程的环境变量为自己所用,所以说环境变量可从父进程传给子进程

    使用export将全局变量导出为环境变量

    两个没有父子关系的 Shell 进程是不能传递环境变量的,并且环境变量只能向下传递而不能向上传递

    总而言之,不同进程之间不能通信!(进程之间通信的问题好像是个难题!)

    通过exit命令可以一层一层地退出 Shell

    # 打开终端,就是父进程
    [c.biancheng.net]$ a=22       #定义一个全局变量
    [c.biancheng.net]$ echo $a    #在当前Shell中输出a,成功
    22
    [c.biancheng.net]$ bash       #进入Shell子进程(bash就是要搞进程执行脚本的)
    [c.biancheng.net]$ echo $a    #在子进程中输出a,失败
    
    [c.biancheng.net]$ exit       #退出Shell子进程,返回上一级Shell
    exit
    [c.biancheng.net]$ export a   #将a导出为环境变量
    [c.biancheng.net]$ bash       #重新进入Shell子进程
    [c.biancheng.net]$ echo $a    #在子进程中再次输出a,成功
    22
    

    通过 export 导出的环境变量只对当前 Shell 进程以及所有的子进程有效,如果最顶层的父进程被关闭了,那么环境变量也就随之消失了

    如果我想让一个变量在所有 Shell 进程中都有效,不管它们之间是否存在父子关系,只有将变量写入 Shell 配置文件中才能达到这个目的!在每次Shell初始化配置环境的时候载入

位置参数

  • 定义 Shell 函数时不能带参数,但是在调用函数时却可以传递参数,这些传递进来的参数,在函数内部就也使用$n的形式接收,例如,$1 表示第一个参数,$2 表示第二个参数

  • 这种通过$n的形式来接收的参数,在 Shell 中称为位置参数,咱们也称他们为“特殊变量”,因为违反变量命名规则嘛

  • 传递参数的时候使用空格分隔

    #!/bin/bash
    #定义函数
    function func(){
        echo "Language: $1"
        echo "URL: $2"
    }
    #调用函数
    func C++ http://c.biancheng.net/cplus/
    

    写这个东西的时候就按其他编程语言的方式写就行

    如果参数个数太多,达到或者超过了 10 个,那么就得用${n}的形式来接收了,例如 ${10}、${23}

特殊变量

  • 上面的位置参数也是特殊变量

    变量含义
    $0当前脚本的文件名。
    $n(n≥1)传递给脚本或函数的参数。n 是一个数字,表示第几个参数。例如,第一个参数是 $1,第二个参数是 $2。
    $#传递给脚本或函数的参数个数。
    $*传递给脚本或函数的所有参数。
    $@传递给脚本或函数的所有参数。当被双引号" "包含时,$@ 与 $* 稍有不同
    $?上个命令的退出状态,或函数的返回值
    $$当前 Shell 进程 ID。对于 Shell 脚本,就是这些脚本所在的进程 ID

    当 $* 和 $@ 不被双引号" "包围时,它们之间没有任何区别,都是将接收到的每个参数看做一份数据,彼此之间以空格来分隔

    "$*"会将所有的参数从整体上看做一份数据,而不是把每个参数都看做一份数据。

    "$@"仍然将每个参数都看作一份数据,彼此之间是独立的

    $?中,退出状态是一个数字,一般情况下,大部分命令执行成功会返回 0,失败返回 1,下面是两个例子,分别获取退出状态和返回值

    #!/bin/bash
    if [ "$1" == 100 ]
    then
       exit 0  #参数正确,退出状态为0
    else
       exit 1  #参数错误,退出状态1
    fi
    
    [roy@localhost demo]$ bash ./test.sh 100  #作为一个新进程运行,不然会直接退出
    [roy@localhost demo]$ echo $?
    0
    
    #!/bin/bash
    #得到两个数相加的和
    function add(){
        return `expr $1 + $2`
    }
    add 23 50  #调用函数
    echo $?  #获取函数返回值
    

    这里的返回方式有问题!

字符串

  • 获取字符串长度

    #!/bin/bash
    str="http://c.biancheng.net/shell/"
    echo ${#str}
    
  • 字符串拼接

    #!/bin/bash
    name="Shell"
    url="http://c.biancheng.net/shell/"
    str1=$name$url  #中间不能有空格
    str2="$name $url"   #如果被双引号包围,那么中间可以有空格
    str3=$name": "$url  #中间可以出现别的字符串
    str4="$name: $url"  #这样写也可以
    str5="${name}Script: ${url}index.html"  #这个时候需要给变量名加上大括号
    

    双引号无敌?

  • 字符串截取

    从指定位置截取,两个参数:除了指定起始位置,还需要截取长度

    url="c.biancheng.net"
    echo ${url: 2: 9}
    # 从右边开始计数
    url="c.biancheng.net"
    echo ${url: 0-13: 9}
    

    从指定字符(子字符串)截取

    # ${string#*chars}  使用 # 号截取右边字符
    url="http://c.biancheng.net/index.html"
    echo ${url#*:}	# //c.biancheng.net/index.html
    
    # 使用 % 截取左边字符,因为是左边字符,所以*在右侧
    url="http://c.biancheng.net/index.html"
    echo ${url%/*}   #结果为 http://c.biancheng.net
    echo ${url%%/*}  #结果为 http:
    
    格式说明
    ${string: start :length}从 string 字符串的左边第 start 个字符开始,向右截取 length 个字符。
    ${string: start}从 string 字符串的左边第 start 个字符开始截取,直到最后。
    ${string: 0-start :length}从 string 字符串的右边第 start 个字符开始,向右截取 length 个字符。
    ${string: 0-start}从 string 字符串的右边第 start 个字符开始截取,直到最后。
    ${string#*chars}从 string 字符串第一次出现 chars 的位置开始,截取 chars 右边的所有字符
    ${string##*chars}从 string 字符串最后一次出现 chars 的位置开始,截取 chars 右边的所有字符
    ${string%*chars}从 string 字符串第一次出现 chars 的位置开始,截取 chars 左边的所有字符
    ${string%%*chars}从 string 字符串最后一次出现 chars 的位置开始,截取 chars 左边的所有字符

数组

  • Shell 并且没有限制数组的大小,理论上可以存放无限量的数据

  • 获取数组中的元素要使用下标[],下标可以是一个整数,也可以是一个结果为整数的表达式

  • 常用的 Bash Shell 只支持一维数组,不支持多维数组

    array_name=(ele1  ele2  ele3 ... elen)
    arr=(20 56 "http://c.biancheng.net/shell/")
    

    Shell有很多让人“不适应”的地方,数组用小括号算一个吧

    同样,赋值号=两边不能有空格

    Shell 是弱类型的,它并不要求所有数组元素的类型必须相同,类型都不要求相同

    ages=([3]=24 [5]=19 [10]=12)
    

    给特定元素赋值,精吗?这里数组的长度就为3,不是11

    # 获取数组元素的值需要使用大括号
    n=${nums[2]}
    # 得到数组的所有元素
    ${nums[*]}
    ${nums[@]}
    # 获取数组长度呗!
    ${#array_name[@]}
    ${#array_name[*]}
    # 如果某个元素是字符串,还可以通过指定下标的方式获得该元素的长度
    ${#arr[2]}		# 类比一下 ${#string_name}
    # 反正#就和长度有关知道吗?
    

    规律就是:$后面只认变量,有其他和变量一起作用的东西包起来才认识,有不是和变量一起作用的包起来隔开才认识!

    *或者@的作用就将元素扩展成列表,搞成列表就方便了?

  • 数组拼接(合并)

    # 先扩展成列表,再合并
    array_new=(${array1[@]}  ${array2[@]})
    array_new=(${array1[*]}  ${array2[*]})
    

    ${}在手,天下我有

  • 删除元素

    unset array_name[index]
    unset array_name		# 删除整个数组
    
  • 关联数组

    关联数组也称为“键值对(key-value)”数组,键(key)也即字符串形式的数组下标,值(value)也即元素值

    # 创建一个叫做 color 的关联数组
    declare -A color
    color["red"]="#ff0000"
    color["green"]="#00ff00"
    color["blue"]="#0000ff"
    # 定义的时候赋值
    declare -A color=(["red"]="#ff0000", ["green"]="#00ff00", ["blue"]="#0000ff")
    

    这个-A是设置变量属性的,后面讲!你会看到后面吗?

    # 访问就是把数字索引换成字符串索引,对于这里使用小括号我也很无奈!
    $(array_name["index"])
    
    # 获取所有元素值
    ${!array_name[@]}
    ${!array_name[*]}
    # 获取所有下标值,这个叹号让人激动
    ${!array_name[@]}
    ${!array_name[*]}
    # 长度
    ${#array_name[*]}
    ${#array_name[@]}
    
  • 到这一步至少要明白变量取值用${}

内置命令

  • 也叫Shell内建命令

    这个叫法吧,都得了解,万一有人装逼用“内建”,你说你不懂就像个傻逼!

  • 之前说Shell解释程序负责的不是命令就是程序,其实命令说的就是内置命令,其他都是程序!

    挺多的,准备好了吗?

    命令说明
    :扩展参数列表,执行重定向操作
    .读取并执行指定文件中的命令(在当前 shell 环境中)
    alias为指定命令定义一个别名
    bg将作业以后台模式运行
    bind将键盘序列绑定到一个 readline 函数或宏
    break退出 for、while、select 或 until 循环
    builtin执行指定的 shell 内建命令
    caller返回活动子函数调用的上下文
    cd将当前目录切换为指定的目录
    command执行指定的命令,无需进行通常的 shell 查找
    compgen为指定单词生成可能的补全匹配
    complete显示指定的单词是如何补全的
    compopt修改指定单词的补全选项
    continue继续执行 for、while、select 或 until 循环的下一次迭代
    declare声明一个变量或变量类型。
    dirs显示当前存储目录的列表
    disown从进程作业表中刪除指定的作业
    echo将指定字符串输出到 STDOUT
    enable启用或禁用指定的内建shell命令
    eval将指定的参数拼接成一个命令,然后执行该命令
    exec用指定命令替换 shell 进程
    exit强制 shell 以指定的退出状态码退出
    export设置子 shell 进程可用的变量
    fc从历史记录中选择命令列表
    fg将作业以前台模式运行
    getopts分析指定的位置参数
    hash查找并记住指定命令的全路径名
    help显示帮助文件
    history显示命令历史记录
    jobs列出活动作业
    kill向指定的进程 ID(PID) 发送一个系统信号
    let计算一个数学表达式中的每个参数
    local在函数中创建一个作用域受限的变量
    logout退出登录 shell
    mapfile从 STDIN 读取数据行,并将其加入索引数组
    popd从目录栈中删除记录
    printf使用格式化字符串显示文本
    pushd向目录栈添加一个目录
    pwd显示当前工作目录的路径名
    read从 STDIN 读取一行数据并将其赋给一个变量
    readarray从 STDIN 读取数据行并将其放入索引数组
    readonly从 STDIN 读取一行数据并将其赋给一个不可修改的变量
    return强制函数以某个值退出,这个值可以被调用脚本提取
    set设置并显示环境变量的值和 shell 属性
    shift将位置参数依次向下降一个位置
    shopt打开/关闭控制 shell 可选行为的变量值
    source读取并执行指定文件中的命令(在当前 shell 环境中)
    suspend暂停 Shell 的执行,直到收到一个 SIGCONT 信号
    test基于指定条件返回退出状态码 0 或 1
    times显示累计的用户和系统时间
    trap如果收到了指定的系统信号,执行指定的命令
    type显示指定的单词如果作为命令将会如何被解释
    typeset声明一个变量或变量类型。
    ulimit为系统用户设置指定的资源的上限
    umask为新建的文件和目录设置默认权限
    unalias刪除指定的别名
    unset刪除指定的环境变量或 shell 属性
    wait等待指定的进程完成,并返回退出状态码
  • 有几个重要命令,可以戳此了解

  • 注:这里的内置命令和普通的Linux终端命令不同,这里是写在Shell脚本中的

    • 举个例子吧
    #!/bin/bash
    read -n 1 -p "Enter a char > " char	# -n num 表示读取num个字符而不是整行	-p 显示后面的提示信息
    printf "\n"  # 换行
    echo $char
    

数学计算

  • Shell 不能直接进行算数运算,必须使用数学计算命令

    接受它好嘛,就是个奇葩!因为,在 Bash中,如果不特别指明,每一个变量的值都是字符串

  • OK,数学计算命令有哪些呢 ?

    运算操作符/运算命令说明
    (( ))用于整数运算,效率很高,推荐使用
    let用于整数运算,和 (()) 类似。
    $[]用于整数运算,不如 (()) 灵活。
    expr可用于整数运算,也可以处理字符串。比较麻烦,需要注意各种细节,不推荐使用。
    bcLinux下的一个计算器程序,可以处理整数和小数。Shell 本身只支持整数运算,想计算小数就得使用 bc 这个外部的计算器。
    declare -i将变量定义为整数,然后再进行数学运算时就不会被当做字符串了。功能有限,仅支持最基本的数学运算(加减乘除和取余),不支持逻辑运算、自增自减等,所以在实际开发中很少使用

    (()) 可以用于整数计算,bc 可以小数计算,其他的别学了,你没空!

    下面开始了解关键字

if&else

  • 最简单的就是只用if

    # 注意分号和结尾的'fi'
    if  condition;  then
        statement(s)
    fi
    # 也可以换行写then,就不加分号了呢
    
    # 如果有两个分支
    if  condition
    then
       statement1
    else
       statement2
    fi
    
    # 任意数目的分支
    if  condition1
    then
       statement1
    elif condition2
    then
        statement2
    elif condition3
    then
        statement3
    ……
    else
       statementn
    fi
    

    if 语句的判断条件,结果不是真就是假,从本质上讲,判断的就是命令的退出状态

    例如condition中$a==$b这个判断会返回一个结果,为 0 表示“成功”,除 0 以外的其它任何退出状态都为“失败”(当然,失败的不是那么彻底,更多的是提示)

    其他语言一般0表示失败,接受吧!就是这么奇葩

    实际上,每一条 Shell 命令,不管是 Bash 内置命令(例如 cd、echo),还是外部的 Linux 命令(例如 ls、awk),还是自定义的 Shell 函数,当它退出(运行结束)时,都会返回一个比较小的整数值给调用(使用)它的程序

    这和函数的返回值是不同的,这只是“状态”

    # 当然,可以使用逻辑运算符组合退出状态
    
    运算符使用格式说明
    &&expression1 && expression2逻辑与运算符,当 expression1 和 expression2 同时成立时,整个表达式才成立。 如果检测到 expression1 的退出状态为 0,就不会再检测 expression2 了,因为不管 expression2 的退出状态是什么,整个表达式必然都是不成立的,检测了也是多此一举。
    ||expression1 || expression2逻辑或运算符,expression1 和 expression2 两个表达式中只要有一个成立,整个表达式就成立。 如果检测到 expression1 的退出状态为 1,就不会再检测 expression2 了,因为不管 expression2 的退出状态是什么,整个表达式必然都是成立的,检测了也是多此一举。
    !!expression逻辑非运算符,相当于“取反”的效果。如果 expression 成立,那么整个表达式就不成立;如果 expression 不成立,那么整个表达式就成立

    和经常一起使用的还有test命令,用来检测某个条件是否成立,详情戳我

    当然,test也可做文件检测还有数值比较

    test 命令比较奇葩,>、<、==只能用来比较字符串,不能用来比较数字,比较数字需要使用 -eq、-gt 等选项;

  • [[ ]]是 Shell 内置关键字

    它和test 命令类似,也用来检测某个条件是否成立

    test 能做到的,[[ ]] 也能做到,而且 [[ ]] 做的更好;test 做不到的,[[ ]] 还能做到

    # 用法
    [[ expression ]]	# 注意前后的两个空格,没有不行!
    # 支持正则表达式
    [[ str =~ regex ]]
    

    如上所示,在 Shell [[ ]] 中,可以使用=~来检测字符串是否符合某个正则表达式

    #!/bin/bash
    read tel	# 读取输入流
    if [[ $tel =~ ^1[0-9]{10}$ ]]
    then
        echo "你输入的是手机号码"
    else
        echo "你输入的不是手机号码"
    fi
    
    • 上面这类代码很常见!!!

case in

  • if作用类似,基本格式如下
    case expression in
        pattern1)
            statement1
            ;;
        pattern2)
            statement2
            ;;
        pattern3)
            statement3
            ;;
        ……
        *)
            statementn
    esac
    

    pattern 可以是一个数字、一个字符串,甚至是一个简单的正则表达式

    为啥是俩分号?匹配内容为哈用半个括号?管他呢!

while&for

  • 循环,知道吗?基本格式如下

    while condition
    do
        statements
    done
    
    #!/bin/bash
    i=1
    sum=0
    while ((i <= 100))
    do
        ((sum += i))
        ((i++))
    done
    echo "The sum is: $sum"
    
  • until 循环

    和 while 循环恰好相反,当判断条件不成立时才进行循环,一旦判断条件成立,就终止循环

    有意思吗?憨批…

    #!/bin/bash
    i=1
    sum=0
    until ((i > 100))
    do
        ((sum += i))
        ((i++))
    done
    echo "The sum is: $sum"
    
  • for循环

    for((exp1; exp2; exp3))
    do
        statements
    done
    

    和C语言风格很类似

    #!/bin/bash
    sum=0
    for ((i=1; i<=100; i++))
    do
        ((sum += i))
    done
    echo "The sum is: $sum"
    
  • for in

    Python风格

    for variable in value_list
    do
        statements
    done
    

    value_list是个列表,in value_list 部分可以省略,省略后的效果相当于 in $@

    这里说列表就类比Python中的概念,融会贯通,还不是纠结的时候

    #!/bin/bash
    sum=0
    for n in 1 2 3 4 5 6
    do
        echo $n
         ((sum+=n))
    done
    echo "The sum is "$sum
    
    #!/bin/bash
    for filename in *.sh
    do
        echo $filename
    done	
    
  • select in

    用来增强交互性,它可以显示出带编号的菜单,用户输入不同的编号就可以选择不同的菜单,并执行不同的功能

    select variable in value_list
    do
        statements
    done
    
    #!/bin/bash
    echo "What is your favourite OS?"
    select name in "Linux" "Windows" "Mac OS" "UNIX" "Android"
    do
        echo $name
    done
    echo "You have selected $name"
    
    # 运行结果
    What is your favourite OS?
    1) Linux
    2) Windows
    3) Mac OS
    4) UNIX
    5) Android
    #? 4↙
    You have selected UNIX
    #? 1↙
    You have selected Linux
    #? 9↙
    You have selected
    #? 2↙
    You have selected Windows
    #?^D
    

    ctrl+D退出
    总结规律:Shell编程有头有尾!fi/done/esac…

  • break&continue

    一般break可以跳出整个循环(一层),continue跳过此次循环(一次)执行下一次!

    但 Shell 中的 break 和 continue 可以跳出多层和多次,注意体会

    break n
    

    n 表示跳出循环的层数,如果省略 n,则表示跳出当前的整层循环(n=1)

    continue n
    

    如果省略 n,continue 会跳过本次循环

    加上n,则会在本层循环内跳过去n次

    #!/bin/bash
    sum=0
    while read n; do
        if((n<1 || n>100)); then
            continue
        fi
        ((sum+=n))
    done
    echo "sum=$sum"
    

    注意区别这里的“层”和“次”

    break是结束某层,循环停止了;continue是跳过某次,循环继续

    和其他语言类似,只不过多了n

函数

  • 和其他语言的函数类似,语法略有差别

    # 定义
    function name() {
        statements
        [return value]
    }
    
    # 调用,反正是不带括号
    name
    name param1 param2 param3	# 多个参数之间以空格分隔
    
    • Shell 中的函数在定义时不能指明参数,但是在调用时却可以传递参数
    • $#可以获取传递的参数的个数,#总代表长度
    • $@或者$*可以一次性获取所有的参数
    #!/bin/bash
    function getsum(){
        local sum=0	# 限定作用域
        for n in $@
        do
             ((sum+=n))
        done
        return $sum
    }
    getsum 10 20 55 15  #调用函数并传递参数
    echo $?
    
    • $?表示函数的返回值,也可以表示上一个命令执行的结果
  • 函数返回值

    函数的返回结果和执行命令同样,是最后一行命令的退出状态,0表示成功,其他表示失败

    说白了Shell 函数不需要写return,或者写成return $?即可

    那么怎么接收函数中返回的计算结果(变量值)呢?有两种方式:

    • 一种是借助全局变量,将得到的结果赋值给全局变量;
    • 一种是在函数内部使用 echo、printf 命令将结果输出,在函数外部使用$()或者反引号捕获结果
    #!/bin/bash
    
    sum=0  #全局变量
    
    function getsum(){
        for((i=$1; i<=$2; i++)); do
            ((sum+=i))  #改变全局变量
        done
    
        return $?  #返回上一条命令的退出状态
    }
    
    read m
    read n
    
    if getsum $m $n; then
        echo "The sum is $sum"  #输出全局变量
    else
        echo "Error!"
    fi
    
    #!/bin/bash
    
    function getsum(){
        local sum=0  #局部变量
        for((i=$1; i<=$2; i++)); do
            ((sum+=i))
        done
       
        echo $sum
        return $?
    }
    
    read m
    read n
    
    total=$(getsum $m $n)
    echo "The sum is $total"
    
    #也可以省略 total 变量,直接写成下面的形式
    #echo "The sum is "$(getsum $m $n)
    

    这里说的捕获就是使用$()将命令的执行结果赋值给变量(命令替换),这个结果是echo输出的而不是函数的最后一行命令的退出状态

    详情参考

小结

  • 以上内容如果全部掌握,就有了坚实的Shell编程基础
  • 下面是结合之前的Linux基础,进入Shell编程高级
  • 总的认识不能忘记:Shell在Linux中是一个程序,像一扇门,任何Linux的命令和程序启动执行都要经过他的解释;但Shell本身又是一门脚本语言,写出来的代码还是要那个Shell程序解释
  • 注:现在的你应该认识到Shell脚本不是单纯Linux命令的堆砌(实际上基本无关),它和Python、Java一样

Shell高级

Shell重定向

  • Linux 中一切皆文件,包括标准输入设备(键盘)和标准输出设备(显示器)在内的所有计算机硬件都是文件。

  • 为了表示和区分已经打开的文件,Linux 会给每个文件分配一个 ID,这个 ID 就是一个整数,被称为文件描述符(File Descriptor)

    文件描述符文件名类型硬件
    0stdin标准输入文件键盘
    1stdout标准输出文件显示器
    2stderr标准错误输出文件显示器
  • 输出重定向是指命令的结果不再输出到显示器上,而是输出到其它地方,一般是文件中

    在输出重定向中,>代表的是覆盖,>>代表的是追加,详解

    fd>之间不能有空格,fd是指文件描述符

  • 输入重定向就是改变输入的方向,不再使用键盘作为命令输入的来源,而使用文件作为命令的输入

  • Linux 文件描述符到底是什么?

    一个 Linux 进程启动后,会在内核空间中创建一个 PCB 控制块,PCB 内部有一个文件描述符表(File descriptor table),记录着当前进程所有可用的文件描述符,也即当前进程所有打开的文件

    除了文件描述符表,系统还需要维护另外两张表:

    • 打开文件表(Open file table)
    • i-node 表(i-node table)

    通过文件描述符(数组下标),可以找到文件指针,从而进入打开文件表

    要想真正读写文件,还得通过打开文件表的 i-node 指针进入 i-node 表

    详情戳此

  • 重定向本质

    通过上面可以发现,打开文件是通过文件描述符(数组下标)找到文件指针(可以理解为指针,本质上还是数组中存储的内存地址嘛),然后进入打开文件表和 i-node 表,这两个表里面才真正保存了与打开文件相关的各种信息

    即文件指针是文件描述符和真实文件之间最关键的“纽带”;这条纽带却非常脆弱,很容易被修改

    Linux 系统提供的函数可以修改文件指针(修改数组值,描述符不动),比如 dup()dup2();Shell 也能修改文件指针,输入输出重定向就是这么实现的

    详情戳此,话说这个指针应该是指向打开文件表和表中的具体位置?

  • 使用exec命令操作文件描述符

    使用 exec 命令可以永久性地重定向,后续命令的输入输出方向也被确定了,直到再次遇到 exec 命令才会改变重定向的方向;

    exec >log.txt将当前 Shell 进程的所有标准输出重定向到 log.txt 文件,它等价于exec 1>log.txt

    重定向的恢复

  • 代码块重定向

    将重定向命令放在代码块的结尾处,就可以对代码块中的所有命令实施重定向

    #!/bin/bash
    sum=0
    while read n; do
        ((sum += n))
    done <nums.txt  #输入重定向
    echo "sum=$sum"
    
    #!/bin/bash
    sum=0
    while read n; do
        ((sum += n))
        echo "this number: $n"
    done <nums.txt >log.txt  #同时使用输入输出重定向
    echo "sum=$sum"
    
    #!/bin/bash
    {
        echo "C语言中文网";
        echo "http://www.beylze.com/d/file/20190908/ggmqcykqgwe.net";
        echo "7"
    } >log.txt  #输出重定向
    
    {
        read name;
        read url;
        read age
    } <log.txt  #输入重定向
    
    echo "$name已经$age岁了,它的网址是 $url"
    

    代码块,就是由多条语句组成的一个整体;for、while、until 循环,或者 if…else、case…in 选择结构,或者由{ }包围的命令

  • Here Document(嵌入文档)

    有时候命令需要处理的数据量很小,将它放在一个单独的文件中有点“大动干戈”,不如直接放在代码中来得方便

    嵌入,就是把数据和代码放在一起,而不是分开存放

    详情戳此

    command <<END
        document
    END
    # command是 Shell 命令,<<END是开始标志,END是结束标志,document是输入的文档(也就是一行一行的字符串)
    
    [roy@localhost ~]$ cat <<END
    > shell教程
    > http://www.beylze.com/d/file/20190908/i5zpldli3og
    > 已经进行了三次改版
    > END
    Shell教程
    http://www.beylze.com/d/file/20190908/i5zpldli3og
    已经进行了三次改版
    
  • Here String(内嵌字符串)

    是 Here Document 的一个变种

    告诉 Shell 把 string 部分作为命令需要处理的数据

    如果 string 中带有空格,则必须使用双引号或者单引号包围

    # 将小写字符串转换为大写
    [roy@localhost ~]$ tr a-z A-Z <<< "one two three"
    ONE TWO THREE
    

组命令

  • 将多个命令划分为一组,或者看成一个整体

    { command1; command2; command3; . . .  }
    (command1; command2; command3;. . . )
    

    由花括号{}包围起来的组命名在当前 Shell 进程中执行,而由小括号()包围起来的组命令会创建一个子 Shell,所有命令都在子 Shell 中执行

    花括号和命令之间必须有一个空格,并且最后一个命令必须用一个分号或者一个换行符结束

  • 组命令可以将多条命令的输出结果合并在一起,在使用重定向和管道时会特别方便

    ls -l > out.txt  #>表示覆盖
    echo "http://www.beylze.com/d/file/20190908/vvtxomnk1w5.html >> out.txt  #>>表示追加
    cat readme.txt >> out.txt
    
    # 组合
    { ls -l; echo "http://www.beylze.com/d/file/20190908/vvtxomnk1w5.html; cat readme.txt; } > out.txt
    (ls -l; echo "http://www.beylze.com/d/file/20190908/vvtxomnk1w5.html; cat readme.txt) > out.txt
    
    # 与管道命令结合,送给lpr命令做输入
    { ls -l; echo "http://www.beylze.com/d/file/20190908/vvtxomnk1w5.html; cat readme.txt; } | lpr
    

子Shell和子进程

  • 在系统中,系统运行的应用程序几乎都是从 init(pid为 1 的进程)进程派生而来的,所有这些应用程序都可以视为 init 进程的子进程,而 init 则为它们的父进程

  • 使用pstree -p命令就可以看到 init 及系统中其他进程的进程树信息(包括 pid)

    CentOS 7 为了提高启动速度使用 systemd 替代了 init

  • 子进程的创建

    使用 fork() 函数可以创建一个子进程;除了 PID(进程ID)等极少的参数不同外,子进程的一切都来自父进程,,包括代码、数据、堆栈、打开的文件等,就连代码的执行位置(状态)都是一样的,这种子进程又被称为子 Shell(sub shell)

    还有一种创建子进程的方式,就是子进程被 fork() 出来以后立即调用 exec() 函数加载新的可执行文件,而不使用从父进程继承来的一切

    详情戳此

进程替换

  • 和命令替换类似,命令替换是把一个命令的输出结果赋值给另一个变量,进程替换则是把一个命令的输出结果传递给另一个(组)命令

    # echo 命令在父 shell 中执行,而 read 命令在子 Shell 中执行,当 read 执行结束时,子 Shell 被销毁,REPLY 变量也就消失了
    echo "http://www.beylze.com/d/file/20190908/y5jvity5mfg.html | read
    echo $REPLY
    

    那怎么解决这个不同父子进程中变量的共享问题呢?

    进程替换有两种形式:

    # 借助输入重定向,它的输出结果可以作为另一个命令的输入
    <(commands)
    # 例如
    read < <(echo "http://c.biancheng.net/shell/)
    echo $REPLY
    
    # 借助输出重定向,它可以接收另一个命令的输出结果
    >(commands)
    echo "C语言中文网" > >(read; echo "你好,$REPLY")
    

    第一个<或者>都是重定向

    注意空格

  • 进程替换的本质

    为了能够在不同进程之间传递数据,实际上进程替换会跟系统中的文件关联起来,这个文件的名字为/dev/fd/n(n 是一个整数)。该文件会作为参数传递给()中的命令,()中的命令对该文件是读取还是写入取决于进程替换格式是<还是>

    该文件是系统内部文件,我们一般查看不到,一般为/dev/fd/63

    即就是这个文件起到了数据中转或者数据桥梁的作用

管道详解

  • 可以将两个或者多个命令(程序或者进程)连接到一起,把一个命令的输出作为下一个命令的输入,以这种方式连接的两个或者多个命令就形成了管道(pipe)

  • 管道使用竖线|连接多个命令,这被称为管道符

    mysqldump -u root -p '123456' wiki | gzip -9 | ssh username@remote_ip "cat > /backup/wikidb.gz"
    

    上面命令做了数据库的备份压缩和安全拷贝,如果分开执行每一次都会产生临时文件,使用管道就避免了创建临时文件

  • 使用了管道的命令有如下特点

    命令的语法紧凑并且使用简单。

    通过使用管道,将三个命令串联到一起就完成了远程 mysql 备份的复杂任务。

    从管道输出的标准错误会混合到一起

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-a7nfTbVa-1618755742141)(C:\Users\szspl\Desktop\secureCopy.png)]

  • 注意重定向和管道的区别

    command > file
    command1 | command1
    

    不要随便尝试command1 > command1,可能会损坏命令的程序

  • 实例

    # 查看文件 log.txt 是否存在于当前目录下
    [c.biancheng.net]$ ls | grep log.txt
    log.txt
    
    # 也可以将最后的输出重定向
    [c.biancheng.net]$ ls -al | grep log.txt >output.txt
    [c.biancheng.net]$ cat output.txt
    -rw-rw-r--.  1 mozhiyan mozhiyan    0 4月  15 17:26 log.txt
    
    # 也可以使用重定向做输入
    [c.biancheng.net]$ tr a-z A-Z <os.txt | sort | uniq
    

    总之,别直接将命令用重定向符号连接

过滤器

  • 将几个命令通过管道符组合在一起就形成一个管道,通过这种方式使用的命令就被称为过滤器

    严谨的说,如果一个 Linux 命令是从标准输入接收它的输入数据,并在标准输出上产生它的输出数据(结果),那么这个命令就被称为过滤器,过滤器通常与 Linux 管道一起使用

  • 常用过滤器命令

    命令说明
    awk用于文本处理的解释性程序设计语言,通常被作为数据提取和报告的工具。
    cut用于将每个输入文件(如果没有指定文件则为标准输入)的每行的指定部分输出到标准输出。
    grep用于搜索一个或多个文件中匹配指定模式的行。
    tar用于归档文件的应用程序。
    head用于读取文件的开头部分(默认是 10 行)。如果没有指定文件,则从标准输入读取。
    paste用于合并文件的行。
    sed用于过滤和转换文本的流编辑器。
    sort用于对文本文件的行进行排序。
    split用于将文件分割成块。
    strings用于打印文件中可打印的字符串。
    tac与 cat 命令的功能相反,用于倒序地显示文件或连接文件。
    tail用于显示文件的结尾部分。
    tee用于从标准输入读取内容并写入到标准输出和文件。
    tr用于转换或删除字符。
    uniq用于报告或忽略重复的行。
    wc用于打印文件中的总行数、单词数或字节数

    常见用法戳此

    说白了就是一堆废话!

  • 如何检测子Shell和子进程?戳此了解

    $变量可以显示进程ID,但要注意:在普通的子进程中,$ 确实被展开为子进程的 ID;但是在子 Shell 中,​$ 却被展开成父进程的 ID,是个坑!

    BASH_SUBSHELLSHLVL,用它们来检测子 Shell 非常方便

信号

  • 信号被用于进程间的通信。信号是一个发送到某个进程或同一进程中的特定线程的异步通知,用于通知发生的一个事件

  • 在 Linux 中,信号在处理异常和中断方面,扮演了极其重要的角色

    哪些情况会引发信号:

    1. 键盘事件 ctrl + c ctrl + \
    2. 非法内存 如果内存管理出错,系统就会发送一个信号进行处理
    3. 硬件故障 同样的,硬件出现故障系统也会产生一个信号
    4. 环境切换 比如说从用户态切换到其他态,状态的改变也会发送一个信号,这个信号会告知给系统

    其他就是主动发信号了!

  • 当进程收到一个信号时,可能会发生以下 3 种情况:

    进程可能会忽略此信号。有些信号不能被忽略,而有些没有默认行为的信号,默认会被忽略

    进程可能会捕获此信号,并执行一个被称为信号处理器的特殊函数

    进程可能会执行信号的默认行为。例如,信号 15(SIGTERM) 的默认行为是结束进程

  • 当一个进程执行信号处理时,如果还有其他信号到达,那么新的信号会被阻断直到处理器返回为止

  • 每个信号都有以SIG开头的名称,并定义为唯一的正整数

    输入kill -l命令,将显示所有信号的信号值和相应的信号名

    信号值被定义在文件 /usr/include/bits/signum.h 中,其源文件是 /usr/src/linux/kernel/signal.c

    信号值和信号名是按规则的逻辑上的定义,可以暂时理解为调用其他程序(信号处理)的触发器,并不需要结合硬件考虑,但需要理解数据结构相关的内容,了解信号如何在进程间传递信息

    信号是为进程服务的,首先需要理解Linux下的PCB(进程控制块)

  • Linux下的PCB

    进程控制块(PCB)是系统为了管理进程设置的一个专门的数据结构(task_struct),用来记录进程的外部特征,描述进程的运动变化过程

    系统利用PCB来控制和管理进程,所以PCB是系统感知进程存在的唯一标志。进程与PCB是一一对应的

    别把进程想的太复杂!至于是怎么调度起硬件资源的,就是和内核CPU的事情了

  • 使用 disown 命令可以将某个作业从列表中删除

    在Shell向各作业发送SIGHUP信号的时候(意思是Shell要退出),就可以跳过这个作业让他继续执行着!

    详情戳此

  • Linux进程是什么?

    当你在 Linux 系统中执行一个程序时,系统会为这个程序创建特定的环境。这个环境包含系统运行这个程序所需的任何东西,这个环境就叫一个进程;即所有命令和程序的执行都会启动一个新的进程

    每个运行的 Shell 都分别是一个进程。当你从 Shell 调用一个命令时,对应的程序就会在一个新进程中执行

    进程都有它自己的生命周期,比如,创建、执行、结束和清除。每个进程也都有各自的状态

    操作系统通过被称为 PID 或进程 ID 的数字编码来追踪进程(唯一)

    运行进程有两种方式:前台和后台

    进程的查看在《Linux基础》中说过,主要是ps auxps -le

    向进程发送信号

  • trap捕获信号

    # 命令格式
    trap command signal
    

    command表示接收到指定信号时将要采取的行动,signal是要处理的信号名

    例如:在脚本执行时按下CTRL+C,将显示"program exit…"并退出(CTRL+C的信号是SIGINT),咋们想忽略这个信号,只需要trap '' 2,表示在捕获到这个信号时不采取任何动作!

Shell脚本模块化

  • 就是将代码分散,类似于Python的import,这里使用source
  • 详情戳我

history命令

  • 使用叹号定位法可以调用任意历史命令

    [roy@localhost ~]$ history
        7  ls
        8  vi test.go
        9  ls
       10  history
       11  export HISTTIMEFORMAT='%F %T'
       12  histroy
       13  export HISTTIMEFORMAT='%F %T'
       14  history
       15  export HISTTIMEFORMAT=
       16  man grep
       17  ls
       18  ll
       19  history
    [roc@roclinux ~]$ !16
    
  • 更多配置请戳

CGI

  • 公共网关接口:Common Gateway Interface
  • CGI 应用程序能与浏览器进行交互,说白了,这是一种服务器规范,可以用任何语言,按照这种规范编写服务器,从而与用户交互
  • 在Ubuntu上安装nginx和fcgiwrap
    apt install -y fcgiwrap	# 如果不能下载把虚拟机改为NAT模式联网
    # 启动
    nohup fcgiwrap -f -c 4 -s unix:/run/fcgiwrap.socket &
    nohup fcgiwrap -f -c 4 -s tcp:192.168.154.130:9000 &	# 推荐(虚拟机IP)
    # 两种方式效果相同,都是为了让nginx找到fcgi建立通信
    # 第二种,nginx和fcgi可以不在同一台机器上
    # 会提示 nohup:ignoring input and appending output to 'nohup.out'
    # 退出
    killall -9 fcgiwrap
    
    • 可以查看参数信息
      在这里插入图片描述
    • 需要nginx作为web服务器,才能使用fcgiwrap与浏览器交互(?)个人感觉这类似Django的uwsgi,应该能直接访问呀!
    • 配置nginx,在conf.d/即可,新建fcgi.conf
      linux1
    • /etc/nginx目录下的fastcgi_params中保存了很多变量信息,可以使用;包括了$document_root和$fastcgi_script_name
    • fastcgi_param指定的是脚本文件的路径,$document_root就是root指定的路径
    • 重启nginx:systemctl restart nginx,如果出错是因为配置没写对!
    • 如果报503服务器错误,修改nginx的启动用户和socket所属用户(一致)
      # 503服务器错误(正在维护/CPU占用大导致)
      sudo chown roykun:roykun /run/fcgiwrap.socket
      # nginx.conf
      user roykun;
      
    • 此时还是不能访问,报403(禁止访问),因为我们还没创建index.cgi文件
      #!/bin/bash
      . ./echo_env.sh
      echo_env
      # 架子啊echo_env.sh,并执行里面的echo_env函数
      
    • 写Shell脚本
      echo_env(){
          echo '$request_method:' $request_method;
          echo '<br>'
          echo 'Hello fcgi'
      }
      # 配置nginx要使用nginx定义和加载的变量,这里是shell,可以使用系统变量,详见我的博客
      # 《Linux Shell》
      
    • 一直报403,不太清楚是什么问题!可能nginx那有点问题
      # 完整报错信息 error.log
      2021/04/21 03:52:59 [error] 25326#25326: *6 FastCGI sent in stderr: "Cannot get 
      script name, are DOCUMENT_ROOT and SCRIPT_NAME (or SCRIPT_FILENAME) set and is the 
      script executable?" while reading response header from upstream, client: 127.0.0.1, 
      server: _, request: "GET /cig/ HTTP/1.1", upstream: "fastcgi://unix:/run/fcgiwrap.socket:", host: "127.0.0.1:8088"
      
      • 修改配置文件后访问127.00.0.1:8088/cgi/index.cgi可以下载,但不能访问/cgi/
      • 之前第一种方式nohup挂载fcgiwrap没出现四个进程?第二种没问题,应该是那个socket文件被整坏了
      • 从监听来看没问题,也更改了nginx的user
        linux3
      • 目前的配置,也没发现问题
        linux4
        # 报错信息
         *1 directory index of "/var/www/fcgi/cgi/" is forbidden, 
         client: 127.0.0.1, server: localhost, request: "GET /cgi/ HTTP/1.1", 
         host: "127.0.0.1:8088"
        
      • 说明目前路径能找到了,主要是forbidden的问题;因为tcp开启才能挂载,所以如下配置:
        Linux6
      • 浏览器找nginx,nginx推给9000,没错啊!现在报502 Bad Gateway;
      • 问题解决,上面配置无误,最后是index.cgi脚本问题,修改如下:
        #!/bin/bash
        html(){     # 定义一个函数
                echo -e "Content-Type: text/html;charset=utf-8\n\n"
        }
        
        get(){
                html    # 指定函数
                echo "get 方法"   # 打印
                if [[ -n $QUERY_STRING ]]; then    # $QUERT_STRING就是get请求发来的查询字符串;-n就代表键值对个数,用来判断有没有!
                        echo $QUERY_STRING
                fi
        }
        
        post(){
                html
                echo "post 方法"
                if [[ -n $POST_DATA ]]; then    # 判断是post
                        echo $POST_DATA
                fi
        }
        
        cgi(){
                if [ "$REQUEST_METHOD" = "GET" ];then    # 请求方式是get的话
                        get "$REQUEST_METHED"
                elif [ "$REQUEST_METHOD" = "POST" ];then
                        read -n $CONTENT_LENGTH POST_DATA
                        post "$POST_DATA"
                fi
        }
        
        cgi
        
    • 直接访问:http://127.0.0.1:8088/cgi/?name=roy&age=18即可 (name=roy&age=18 )
    • 上面是Shell脚本,怎么写参考上面即可

小结

  • 到此为止,基本学习了Shell编程的所有内容,可以对每个点更深入的探索,这也需要结合操作系统和计算机组成原理的知识
  • 实践是检验并巩固学习成果的最好方式
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

瑞士_R

修行不易...

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值