• Shell与脚本


    shell是Linux操作系统的用户接口,我们经常需要编写脚本让操作系统自动执行一系列指令的需求,本文将简单介绍开发shell脚本的所需的语言特性。

    shell脚本是指令序列,其指令可以直接在终端中执行。同样地,终端中的指令也可以写到脚本中。

    脚本文件通常以.sh作为后缀名,第一行以#!开头指定执行脚本的程序:

    #!/usr/bin/bash
    

    #!开头的第一行被称为Hashbang或Shebang。 而#是shell脚本中的行注释符。

    通常有三种执行脚本的方式:

    • sh start.sh: 在终端中创建一个sh子进程执行脚本, 执行者需要拥有脚本的读权限。
      该方式实际上是将脚本路径作为参数传递给了sh命令。
    • source start.sh: 在终端中执行脚本,相当于将脚本中的指令逐条复制到终端执行。
      脚本中局部变量将保留在终端环境变量中, 脚本的pid和工作目录等环境也与终端一致。
    • ./start.sh: 根据HashBang指定的程序,在子进程中执行脚本。

    sh命令有一些有用的选项帮助我们开发和调试脚本:

    • sh -n start.sh:对脚本进行语法检查, 不实际执行脚本
    • sh -x start.sh: 把将要执行的命令输出到stderr便于进行调试

    变量

    shell中变量是弱类型的, 变量名只能包含字母、数字或下划线"_",首字符只能为字母。

    shell主要面向文本处理而非数据计算,因此变量默认类型为字符串型。

    A=abc
    echo $A
    

    变量在使用前无需声明,在为变量赋值时=左右不能添加空格。

    A = abc会被shell解释为执行指令A,参数为=abc

    $为变量标志符, echo $A指令将显示变量A的内容abc, 为了明确指定变量名也可以写作${A}

    A=a
    AB=ab
    echo ${A}B
    

    $(cmd)可以把命令的输出作为返回值, 如:

    PWD=$(pwd)
    

    变量$PWD存储了当前的工作目录路径。

    在shell中可以直接书写字符串,但仍建议用单引号或双引号标识字符串。

    在单引号标识的字符串中$不被作为变量标识符, 而双引号则会将$替换为变量内容。

    A="abc"
    echo '$A' # $A
    echo "$A" # abc
    

    字符串拼接不需要任何运算符,只需要将它们写在一起即可:

    A="abc"
    B="123"
    echo "$A+$B"  # abc+123
    echo "$A$B"  # abc123
    echo "$Adef"  # abcdef
    

    全局变量

    变量按照作用域可以分为局部变量和全局变量,上文示例中定义的变量都是局部变量, 作用域仅限执行脚本的进程。

    子进程可以继承父进程的全局变量,export指令用于定义全局变量:

    export A=abc
    

    整型变量

    shell仅支持整型计算, declare命令可以声明整型变量,let指令用于算术运算:

    declare -i a=1
    let a=a+1
    echo $a  # 2
    let a+=1
    echo $a  # 3
    

    let指令支持算术运算符包括:

    • +:加法
    • -: 减法
    • *: 乘法
    • /: 除法
    • **: 乘方
    • %: 取余

    let指令也支持算术运算符对应的算术赋值运算符,如+=

    数组

    bash中可以使用圆括号定义数组,元素之间用空格分割,数组下标从1开始:

    arr=(1 'a' "abc")
    echo ${arr[1]}
    

    也可以直接使用下标定义数组:

    arr2[1]=1
    arr2[2]=2
    

    该方法同样可以用于修改已存在的数组。

    特殊变量

    shell中预定义了一些特殊变量,通过这些变量可以获得环境信息:

    • $$: 执行脚本的进程ID(pid)
    • $?: 上一条命令的返回值
    • $!: 上一条后台指令的执行进程的ID

    上述变量在交互式终端中同样有效。

    还有一些变量可以获得执行脚本时传入的参数:

    • $0: 脚本的文件名
    • $1~$n: 传给脚本的第n个参数
    • $#: 传入参数的个数
    • $@: 参数列表
    • $*: 单个字符串形式的参数列表

    流程控制

    if

    declare -i a=90
    if [ $a -gt 80 ]; then
        echo "A"
    elif [ $a -lt 60 ]; then
        echo "C"
    else
        echo "D"
    fi
    

    结束标志fi即是if反写, 我们还将在其它地方遇到bash的这种命名风格。

    注意,[]旁边的空格不可省略。

    -lt, -gt用于进行整型的大小比较:

    • -eq: 等于(equal)
    • -ne: 不等于(not equal)
    • -gt: 大于(greater)
    • -ge: 大于等于(greater-equal)
    • -lt: 小于(less)
    • -le: 小于等于(less-equal)

    进行复合逻辑判断也很简单:

    if [ $a -gt 60 -a ( ! $a -gt 90 -o $a eq 91 ) ]; then
        echo "make no sense"
    fi
    
    • !: 非
    • -a: 且and
    • -o: 或-o

    逻辑运算遵循短路计算原则。

    <, >等运算符在[]中只能用于字符串的比较, 而在[[]]<, >可以用于整型和字符串的大小比较, 也可以使用&&||来书写逻辑表达式。

    if的条件判断不一定使用[][[]]表达式,它可以是任何一个命令。命令的返回值为0则if判断为真, 非0判断为假。

    [][[]]转义表达式也可以像普通指令一样执行,判断为真则返回0,假则返回非0值。

    [ 2 -gt 1 -a 3 -lt 4 ] && echo 'ok'
    

    除此之外,if还可以进行更多种类的条件判断:

    判断字符串相等

    if [ ${NAME} = 'tmp' ]; then
        echo "name is tmp"
    fi
    

    判断文件是否存在

    if [ -e tmp ]; then
        echo "tmp exists"
    fi 
    

    判断tmp是否存在,tmp可以是目录或文件。

    判断是否为普通文件

    if [ -f tmp ]; then
        echo "file tmp exists"
    fi
    

    判断tmp是否为文件,tmp不能是目录。

    判断是否为目录

    if [ -d tmp ]; then
        echo "directory tmp exists"
    fi
    

    判断是否具有执行权限

    if [ -x tmp ]; then
        echo "tmp is executable"
    fi
    

    不判断文件是否可执行,只判断是否拥有x权限。 因此,tmp为有x权限的目录时也会判断为真。

    类似的还有,-w判断是否拥有写入权限, -r判断是否拥有读取权限。

    判断是否为空文件

    if [ -s tmp ]; then
        echo "file is not empty"
    fi
    

    case

    case类似于其它语言中的switch语句:

    case ${NAME} in
        'a')
            echo "name is a"
            ;;
        'b')
            echo "name is b"
            ;;
        *)
            echo "other names"
            ;;
    esac
    

    从第一个匹配的标签开始执行, 两个标签之间必须有;;*)是其它标签都不匹配时的默认标签。

    for

    for循环可以遍历一个序列:

    for i in $(seq 1 10); do
        echo $i
    done
    # echo: 1 2 3 ... 10
    

    一些命令的输出也可以作为序列:

    for i in $(ls); do
      echo $i
    done
    

    遍历所有参数:

    for arg in "$@"; do  
        echo $arg  
    done 
    

    另一种形式的for循环:

    for (( i=0; i<100; i++)); do
        echo $i
    done
    

    while

    declare -i i=0
    while [ $i -lt 10 ]; do
        echo $i
        i=$i+1
    done
    

    while(true)这样的死循环也很容易:

    declare -i i=0
    while ; do
        echo $i
        [ ! $i -lt 10 ] && break
        i=$i+1
    done
    

    函数

    shell提供了定义函数的功能, 函数就像是脚本中的子脚本:

    range() {
        for (( i=0; i<${1}; i++)); do
            echo $i
        done
        return ${1}
    }
    
    range 100
    

    函数同样使用位置参数$1~$n来访问参数,$0为函数的名称, $@, $#等变量的含义不变。

    进程间通信

    管道

    管道用于将上一条指令的输出作为下一条指令的输入:

    ls | grep ".zip"
    

    xargs

    有一些指令不支持使用管道传递参数,因此需要xargs命令

    find ~ | xargs ls 
    

    xargs会以空格为分隔符将输入分隔为参数,然后将参数传给ls。

    重定向

    重定向用于将命令的输入输出从标准流重定向到文件。标准流包括:

    • stdin: 标准输入流,文件描述符为0
    • stdout: 标准输出流,文件描述符1
    • stderr: 标准错误流,文件描述符2

    输出到文件,覆盖原有内容:

    echo "hello" > 1.txt 
    

    输出到文件, 追加到文件尾:

    echo "hello" >> 1.txt
    

    从文件输入:

    wc -l < 1.txt
    

    重定向标准错误输出流:

    cmd 2> 2.txt
    

    将标准错误输出追加到文件:

    cmd 2>> 2.txt
    

    将标准错误和标准输出一同重定向到文件:

    cmd > 1.log 2>&1
    

    2>&1是将stderr重定向到stdout。

    后台执行

    shell可以执行一行指令后立即返回, 返回后可以通过$?变量获得执行进程的ID:

    $ sleep 10 &
    [1] 79403
    $ echo $!
    79403
    
  • 相关阅读:
    正确使用SqlConnection对象,兼谈数据库连接池
    简单设计实现基于Forms认证的注册登录等用户基础服务
    简单利用Memcached进行缓存层设计
    殊途同归,ado.net快速实现MySql的CRUD
    【数据库设计】“Max加一”生成主键的注意点
    利用FastReflectionLib快速实现对象克隆
    容易遗忘的一些小代码之 Cross apply and Outer apply
    OBJECT_ID 有哪些种类
    BIWORK 分区表阅读与实践笔记
    容易遗忘的一些小代码之 Merge Operation and Output Clause
  • 原文地址:https://www.cnblogs.com/Finley/p/8442628.html
Copyright © 2020-2023  润新知