bash 速描

  • bash 速描

  • 资料来源:

    https://wangdoc.com/bash/index.html

  • 更新

    1
    2
    3
    4
    5
    6
    20.05.19 初始化
    20.05.22 变量
    20.05.23 字符串
    20.05.24 行操作
    20.05.26 条件判断
    20.05.27 函数

导语

  • 重刷一遍 bash.
  • 最开始接触 bash 是跟着 鸟哥的 linux 私房菜,之后没有太多应用就放弃了.然而现在又到了大量需要时候..
  • 正好阮一峰老师的 bash 教程 发布了,wsl2 也随着 win10 2004 到了正式版中,条件成熟,开搞.

简介

  • shell,这个词听说了很久,所谓 shell ,相对于 kernel 而言,就是壳的意思.
  • shell 可以说是用户通过命令与内核交互的途径.shell 可以有很多种,我们熟悉的 sh bash zsh 就是几种常见的 shell 类型.
  • 使用最广的自然是 bash 了 (其实挺想学学 zsh 的,但是别着急).
  • 2019 年 bash 最新版本是 5.0.3(1)-release.
  • 这一篇仅仅是 bash 的速描,而非教程.

基础语法

  • echo(ow 新英雄😂)
    • 文本输出到屏幕
    • 多行需要 “”
    • -n: 去掉默认会在文本末尾添加的 \n 换行符.
    • -e: 解释 “” 内部的特殊字符,没有 -e 例如 \n 就会直接输出 \n 字符,而不是换行.
  • 命令格式
    • 基本上是 $ command [ arg1 … [ argN ]]
    • 命令 + 参数 1 ..参数 N
    • 一般参数有两种
      • -: 短形式,简写,在命令行居多.
      • --: 长形式,脚本中居多.
    • 多行编辑时,通过 \ 连接多行命令,执行时会合并成一行.
  • 空格
    • bash 的不同参数之间以空格区分
    • 会忽略多个空格
  • 分号
    • 分割两个命令
    • 即使第一个命令执行失败,第二个命运也会继续执行.
  • 命令组合
    • Command1 && Command2: 只有命令 1 执行成功,才执行命令 2.
    • Command1 || Command2: 不管命令 1 执行如何,始终执行命令 2.
  • type 命令
    • 可以区分命令本身来源于内置命令,还是外部程序.
    • type 本身也是内置命令
    • -a: 输出命令的全部定义
    • -t: 返回命令的全部类型,
      • 别名 alias
      • 关键字 keyword
      • 函数 function
      • 内置命令 builtin
      • 文件 file
  • 快捷键
    • Ctrl + L: 清屏,把当前行移动到页面最上位置.
    • Ctrl + C: 中断命令
    • Ctrl + U: 由光标位置,删除到行首
    • Ctrl + K: 由光标位置,删除到行末
    • Ctrl + D: 等于 exit
    • Shift + PageUP: 向上滚动
    • Shift + PageDown: 向下滚动
  • TAB 补全,如果遇到无法补全,检查一下 bash-completion 这个插件,装没装.

模式拓展

  • 如果要完成一项复杂的工作,纯粹的 shell 命令会烦死的,由此引入的模式拓展.
  • 模式拓展,大概是将 ?* 等等通过预定的模式展开成原始的命令,最大的作用是省纸.
  • 有点类似于正则啊,实际上模式拓展要早于正则,但是模式拓展要简单方便很多.
  • 当然模式拓展是可关闭的.set -f 关闭,set +f 打开.
  • 波浪线拓展
    • ~: 用户当前的主目录
    • ~user: 用户 user 的主目录.
    • ~+: =pwd 当前目录
  • ? 字符拓展
    • ?: 文件路径中任意的单字符,不包括空字符.
    • 如果匹配的文件存在才进行转换,否则只是输出成 ? 本身.
  • * 字符拓展
    • *: 文件路径中任意数量的任意字符 (类似正则的 .*)
    • .*: 匹配隐藏文件.
    • .[!.]*: 排除 . .. 以外的隐藏文件,以 . 开头,第二个字符不是 .,之后是任意字符.
    • **/*: 匹配包括任意层级子目录的任意文件.
    • 只有文件确实存在时,才会进行转换,否则只是输出 * 本身.
  • 方括号拓展
    • [..]: 一个方括号内任意字符.
    • [^..][!..]: 任何一个不在方括号内任意字符
    • [[..]: 匹配 [ 这个字符本身.
    • [-..][..-]: 匹配 - 这个字符时,只能放在开头或结尾.
    • 只有文件确实存在时,才会进行转换,否则只是输出 [..] 本身.
  • [start-end] 拓展
    • [a-z]: 所有小写字母任意一个
    • [a-zA-Z]: 所有小写大写字母任意一个
    • [a-zA-Z0-9]: 所有大小写字母和数字任意一个
    • [!a-zA-Z]: 除了英文字母以外的任意一个字符
  • 大括号拓展
    • 非文件拓展,会自动转换成所有值
    • {…}: 自动拓展成大括号内所有值,各个值期使用逗号隔开.逗号前后不能有空格.
    • {j{p,pe}g,png}: 大括号嵌套,可以一定程度上当生成器了
    • {cat,d*}: 大括号可以与其它模式连用,但是大括号总是优先于其他模式.这里就是匹配 catd* 的文件.
  • {start..end} 扩展,类似中括号 [start-end]
    • {a..c}: a b c
    • {1..4}: 1 2 3 4
    • {c..b}: c b a
    • {4..1}: 4 3 2 1
    • {01..5}: 01 02 03 04
    • {0..8..2}: 0 2 4 6 8 (步长是 2)
    • 无法转换的简写,原样输出.
  • 变量拓展
    • 以一个 $ 代表变量值..嗯..kotlin 有这味.
    • $value: 拓展成对应变量值
    • ${value}: 同上.
    • ${!string*}${!string@}: 返回所有匹配给定字符串 string 的变量名
  • 子命令拓展
    • $(…): 将括号内的命令运行结果作为拓展输出.
    • $ `…`: 同上.
    • 可以嵌套.
  • 算术拓展
    • echo $((2 + 2)): 结果拓展为 4 .
  • 字符类
    • [[:class:]]: 表示一个字符类,扩展成某一类特定字符之中的 一个.
    • 当没有匹配时,会直接输出.
    • [[:alnum:]]:匹配任意英文字母与数字
    • [[:alpha:]]:匹配任意英文字母
    • [[:blank:]]:空格和 Tab 键
    • [[:cntrl:]]:ASCII 码 0-31 的不可打印字符
    • [[:digit:]]:匹配任意数字 0-9
    • [[:graph:]]:A-Z、a-z、0-9 和标点符号
    • [[:lower:]]:匹配任意小写字母 a-z
    • [[:print:]]:ASCII 码 32-127 的可打印字符
    • [[:punct:]]:标点符号(除了 A-Z、a-z、0-9 的可打印字符)
    • [[:space:]]:空格、Tab、LF(10)、VT(11)、FF(12)、CR(13)
    • [[:upper:]]:匹配任意大写字母 A-Z
    • [[:xdigit:]]:16 进制字符(A-F、a-f、0-9)
    • 例子:
      • $ echo [[:upper:]]*: 所有以大写字母开头的文件.
      • $ echo [![:digit:]]*: 所有不以数字开头的文件
  • 量词语法
    • 量词语法控制拓展的匹配次数.由 extglob 参数控制,一般是开着的.
    • 当没有匹配时,原样输出.
    • ?(pattern-list):0 或 1
    • *(pattern-list):0 或 多
    • +(pattern-list):1 或 多
    • @(pattern-list):1
    • !(pattern-list):匹配零个或一个以上的模式,但不匹配单独一个的模式 (??)
    • 例子
      • abc?(.)txt: 不取 或 只取第一个符合开头 abc 结尾 .txt 的文件
      • abc+(.txt|.php): 不取 或 匹配多个以 adc 开头,结尾是 .txt 或 .php 的文件
  • 匹配注意点
    • 通配符是先拓展再执行.
    • 文件名拓展不存在匹配时,基本都是按照原样输出.
    • 通配符文件名拓展一般只适用于本层路径.子路径需要声明子路径才行 xx/xx.也可以使用 **/xx,直接匹配所有的文件 (不可控).
    • Bash 允许文件名使用通配符,即文件名包括特殊字符,这时引用文件名,需要把文件名放在单引号里面.fo*.
  • shopt 命令
    • shopt 命令可以调整 Bash 的行为.
      • $ shopt -s [optionname]: 打开某个参数
      • $ shopt -u [optionname]: 关闭某个参数
      • $ shopt [optionname]: 查询某个参数关闭还是打开
    • dotglob: 拓展结果包含隐藏文件
    • nullglob: 拓展没有结果时,返回空字符.
    • failglob: 拓展没有结果时,直接报错,不再执行.
    • extglob: Bash 支持 ksh 的一些扩展语法,量词拓展就是之一.
    • nocaseglob: 通配符拓展不再区分大小写.
    • globsta: ** 匹配零个或多个子目录,默认是关闭的.

引号和转义

  • bash 只有字符串一直类型的变量.对应特殊字符有一套转义的规则,整体和 c/kotlin 差不多.

  • 转义

    • 输出特殊字符 \ + 特殊字符 $ * &
    • \\ 输出自身
    • \a 响铃 (??); \b 退格; \n 换行; \r 回车; \t 制表符 (??)
  • 单引号

    • 单引号内部所有字符均视为普通字符,不会有拓展的效果.
    • 单引号内部使用单引号,$`it\'s' 需要在外层单引号前加 $,同时在 单引号前 + 转义.
    • 比较合理的做法是在双引号中使用单引号.
  • 双引号

    • 大部分特殊字符,双引号会保留原意,除 $ ` \ 除外,会自动拓展.
    • 特别留意 * 在双引号中也只是普通字符,不会进行文件名拓展.
    • 文件名包含空格,必须使用双引号.test kot.txt,而且双引号会原样保持多余空格.
    • 双引号还能保存原始命令的输出格式.如果没有双引号,基本都堆到了单行.echo "$(cal)"
  • Here 文档

    • 一种输入多行字符串的方法.

    • 格式

      1
      2
      3
      << token
      text
      token
      • << token: 开始标记,<< 打头,之后是文件名,文件名任意,但最后一个必须是换行符.
      • token: 结束标记,文件名,必须是定格.
      • 中间是多行的字符串内容
    • Here 内部变量替换,转义字符依旧有效,但拓展和单引号双引号失效.

    • 如果不希望有变量替换,可以将开始标记的文件名加上单引号.<< `example`

    • Here 文档的本质是重定向,将字符串重定向到了某个命令,相当于加了 echo.echo string | command

    • 因此 Here 文档只能适用于那些接收标准输入为参数的命令.

  • Here 字符串

    • 算是 Here 文档的变体,使用 <<< string 将字符串通过标准输入传递给命令.
    • 原理大致相同,等同于添加了 echo.

变量

  • bash 下的变量分为两种,一是环境变量,另一个是自定义变量.

    • 环境变量是 bash 自带的变量,由系统定义.
    • 自定义变量即用户自行定义,当关闭 shell 后就不再存在.
    • env: 输出所有环境变量.
    • echo xx: 输出单个变量.
    • set: 输出所有变量及 bash 函数.
  • 创建变量

    • 变量名通常是大写字母表示.(约定俗成)
    • bash 区分大小写.
    • 字母/数字/下划线.
    • 开头必须是字母/下划线.
    • 不允许空格/标点符号.
    • 所有类型均为字符串.
    • 变量值有空格等,需要单/双引号.
  • 读取变量

    • $xxx${xxx}: 读取变量
    • \$: 要正常使用 $,需要转义.
    • ${!xxx}: 如果变量值本身是另一个变量,可以加 ! 读取到最终的值.
  • 删除变量

    • unset NAME: 删除变量,但是并不怎么好用..变量都是字符串类型,删除后还是空字符串.
    • NAME=''NAME=: 都等同于 unset.
  • 输出变量 (更多是传递变量吧)

    • export xxx: 子 shell 可以读取到父 shell 对应的变量值.而且修改不影响父 shell.(类似继承)
  • 特殊变量

    • 由 shell 定义,用户只能使用不能赋值.
    • $?: 上一个命令的退出码,为 0 ,则上一条命令执行成功.
    • $$: 当前 shell 的 pid.
    • $_: 上一个命令的最后参数.
    • $!: 最近一个后台执行异步命令的进程 id .
    • $0: 当前 shell 的名称.
    • $-: 当前 shell 的启动参数.
    • $@ $#: 脚本的参数数量.
  • 变量的默认值

    • Bash 可以通过默认值防止变量为空.
    • ${varname:-word}: varname 空返回 word.
    • ${varname:=word}: varname 空设置 varname=word 并返回.
    • ${varname:+word}: varname 存在且不为空,返回 word,否则返回空值.这是个测试命令.
    • ${varname:?message}: varname 为空,打印 varname: message 并中断脚本的执行.防止变量未定义.
  • declare 命令

    • declare 可以为变量设置一些限制.像只读,整数类型等.
    • declare OPTION VARIABLE=value: 基本格式
    • -i: 声明整形参数,可以直接运算了.但是即使声明成整形了,依然可以被改写成其他字符串.
    • -x: 等同于 export ,可以将变量继承到子 shell.
    • -r: 声明只读变量,unset 也不行.
    • -u: 声明为大写字母,自动将变量值的小写转大写..(强迫症专用)
    • -l: 声明为小写字母,同上 (强迫症专用)
    • -p: 输出变量信息,不指定会输出全部.
    • -f: 输出当前环境所有函数,包括定义.
    • -F: 输出当前环境所有函数,不包括定义.
  • readonly 命令

    • 单 readonly 等同于 declare -r 只读.
    • 参数
      • -f: 声明变量为函数名
      • -p: 打印出所有只读变量
      • -a: 声明变量为数组
  • let 命令

    • let foo=1+2: 可以直接执行运算.
    • let "foo = 1 + 2": 运算包含空格,需要引号.
    • let "v1 = 1" "v2 = v1++": 多个表达式可以用空格隔离.

字符串操作

  • 字符串是 bash 唯一的变量类型,这一节是对字符串的操作.

  • 字符串长度

    • 实例

      1
      ${#varname} : 输出字符串长度,大括号必须,否则 `$#` 会被拓展成脚本的参数个数.
  • 子串提取

    • ${varname:offset:length}: 返回 varname 的子串,从 offset 长度为 length.索引从 0 开始.
    • {varname:offset:length}: 同上,但 $ 可省略
    • ${varname:offset}: 从 offset 开始一直到结尾.
    • ${varname: offset:length}: 这里 offset 可以为负值,但是前面需要加一个空格隔开.而且 length 必须大于 0.
  • 搜索和替换 (原始变量并不会被改变)

    • ${variable#pattern}: 从头开始,非贪婪匹配,删除匹配部分并返回.
    • ${variable##pattern}: 从头开始,贪婪匹配,删除匹配部分并返回.
    • ${variable%pattern}: 从尾开始,非贪婪匹配,删除匹配部分并返回.
    • ${variable%%pattern}: 从尾开始,贪婪匹配,删除匹配部分并返回.
    • 匹配部分可以有个 * ? [] 等,具体模式与上文相同.
    • 没有匹配时,会直接原样返回.
    • ${variable/pattern/string}: 贪婪匹配,但仅替换第一个匹配.
    • ${variable//pattern/string}: 贪婪匹配,替换所有.
    • ${variable/#pattern/string}: 模式必须在字符串开头
    • ${variable/%pattern/string}: 模式必须在字符串的结尾.
  • 改变大小写

    • ${varname^^}: 转为大写
    • ${varname,,}: 转为小写

算术运算

  • 虽然 bash 变量都是字符串,但是还是能运算的.
  • 算术表达式
    • ((…)): 自动运算双括号内算术,并且忽略算式空格.结果如果为 0 算是命令执行失败.$? 会查询到非 0.
    • $((…)): 这样就算是算术表达式,可以返回算术的值.
    • + - * %: 没啥区别
    • /: 整除,结果是整数.
    • **: 指数
    • ++ --: 与 c 相同,有前后之分.前面有命令根据位置会有先运算还是先执行命令的差别.
    • $(( (2 + 3) * 4 )): 内部可以使用括号,优先级最高.
    • echo $(($((5**2)) * 3)): $((…)) 可以嵌套,但是只能计算整数,否则报错.
    • 算术表达式内可以加 $varname,也可以不加 varname,如果对应的变量并不存在,对应会返回空值.
    • ps: 还有一种写法是 $[…],已经过时了,不要再用了.
  • 进制
    • bash 默认是十进制,当然也能使用其他进制.
    • 0number: 8 进制
    • 0xnumber: 16 进制
    • base#number: base 进制的数.
  • 位运算 (跟 c 差不多)
    • <<: 左移
    • >>: 右移
    • &: 与运算
    • |: 或运算
    • ~: 取反
    • ^: 亦或
    • 位运算一般配合 8/16 进制数.
  • 逻辑运算
    • <; >; <=; >=; ==; !=; &&; ||; !: 与 c 完全相同.
    • expr1?expr2:expr3: 支持三目运算符让我有点意外.
    • 逻辑表达式为真返回 1,假返回 0.(跟 c 一样)
  • 赋值运算
    • $((…)): 执行赋值运算.
    • 支持的运算符 =; +=; -=; *=; /=; %=; <<=; >>=; &=; |=; ^=;
    • 跟 c 也一样.
  • 求值运算
    • echo $((foo = 1 + 2, 3 * 4)): 在 $((…)) 双括号内, , 是求值运算符.
    • 上面的表达式会输出 12 ,然后对 foo 赋值.
  • expr 命令
    • 有时双括号有点眼花 (◎﹏◎).
    • 可以使用 exper xxx 代替
    • 支持变量替换,但不支持非整数参数.

行操作

  • bash 内置了 Readline 库,行操作基于此,默认是 Emacs 快捷键,没必要不需要改.

  • 光标移动 (alt 可由 esc 代替)

    • Ctrl + a:移到行首
    • Ctrl + b:向行首移动一个字符,与左箭头作用相同
    • Ctrl + e:移到行尾
    • Ctrl + f:向行尾移动一个字符,与右箭头作用相同
    • Alt + f:移动到当前单词的词尾
    • Alt + b:移动到当前单词的词首
  • 清屏

    • ctrl + l: 等同于 clear
  • 编辑操作

    • 编辑行的内容
    • ctrl + d: 删除光标位置的字符.千万小心,当前行没有字符,直接退出终端
    • ctrl + w: 删除光标前的单词
    • ctrl + t: 光标位置字符与前一位交换
    • alt + t: 光标位置词与前一位词交换
    • alt + l: 光标位置到词尾转为小写
    • alt + u: 光标位置到词尾转为大写
  • 自动补全

    • Tab: 不说了,连恩两次,列出所有可能补全.
    • alt + tab: 从 bash_history 里提取补全,但是吧和多任务切换冲突了…
    • 其他用的就少了
  • 操作历史

    • 所有历史命令都保存在 ~/.bash_history 文件中.

    • 上下键切换,不多说了.

    • history: 显示 history 文件

    • history -c: 清理 history

    • ctrl + r: 会显示显示操作历史,键入命令可显示历史文件中查询的命令.常用.

    • 快捷键

      • ctrl + o: 执行当前条目,并显示下一条命令
    • !快捷键

      • !string: 相当于搜索历史,找出最近的以 string 开头的命令并执行.但只会匹配命令,没有参数什么事
      • !?string: 执行最近一条包含字符串 string 的命令
      • ^string1^string2: 执行最近一条包含 string1 的命令,将其替换成 string2
      • !n: 执行 history 第 n 行的命令
      • !-n: 执行 history 末尾开始 -n 行命令
  • 其他快捷键

    • Ctrl + v: 下一个输入的特殊字符变成字面量..
    • Alt + .Alt + _: 非常重要,插入上一条命令的最后一个词.对于很长文件路径非常适用.

目录堆栈

  • 大意是有个堆栈可以把目录当作变量一样操作.进栈出栈.
  • cd -: 返回前一次进入的目录
  • pushd dir: 进入 dir 同上 dir 放入堆栈.
  • popd: 取栈顶目录,出栈并进入该目录.
  • popd -n: 仅删除栈顶目录,不改变当前 shell 路径.
  • pushd +3: 从栈顶起,第三个目录移动到栈顶.
  • pushd -3: 从栈底起,第三个目录移动到栈顶.
  • popd +3: 从栈顶起,删除第三个目录.
  • popd -3: 从栈底起,删除第三个目录.
  • dirs 命令
    • 显示目录堆栈
    • -c: 清空目录堆栈
    • -l: 用户主目录不显示波浪号前缀,而打印完整的目录 (???)
    • -p: 打印目录堆栈.
    • -v: 打印目录堆栈带编号.
    • +N: 显示自栈顶起,第 N 个目录.
    • -N: 显示自栈底起,第 N 个目录.

脚本入门

  • 脚本就不多说了,学习 bash 就是为了写个脚本,各类自动化真是羡慕.

  • Shebang 行

    • 就是脚本第一行,实际作用是指示脚本解释器.一般是 sh 或 bash
    • #!/bin/sh#!/bin/bash : 有没有空格都行.
    • #!/usr/bin/env bash: 阮老师推荐这样写,以防万一 bash 解释器没有在 /bin 下面.
    • 这一行不是必须的,只要不通过 ./script.sh 这样调用.
  • 权限和路径

    • chmod +x script.sh: 所有用户,执行权限

    • chmod +rx script.shchmod 755 script.sh: 所有用户.读 执行权限

    • chmod u+rx script.shchmod 700 script.sh: 仅所有者,读 执行权限.

    • 路径,新建 ~/bin 存放脚本,加入 PATH ,这样可以直接输入脚本名执行脚本了.

      1
      2
      mkdir ~/bin
      export PATH=$PATH:~/bin
  • env 命令

    • #!/usr/bin/env bash: shell 查找 bash 的路径并加载,如果 bash 的解释器没有在 /bin 下,可以兼容.
    • 参数
      • -i: 不带环境变量启动
      • -u xx: 从环境变量中删除一个变量.
  • 注释

    • 所有注释都是以 # 开头,可以在结尾,也可以另起一行.
    • 建议在脚本开头,说明当前脚本的作用.
  • 脚本参数

    • 调用脚本时后面跟随的参数.bash 对这种情况进行了处理.

    • $0: 脚本名,脚本自身名称,不同的调用方式名称不同.

    • $1 ~ $9: 脚本第一个到第九个参数.

    • $#: 参数的总数

    • $@: 全部参数,参数之间空格隔开.

    • $*: 全部参数,参数使用 $IFS 的第一个字符隔开,默认是空格.

    • ${10} ~ ${xx}: 10 个以后的参数可以加大括号引用.

    • command -o foo bar: 参数依次是 -o foo bar

    • 多个参数在同一个双引号中,算是一个参数.

    • 也可以使用 for 循环 $@ 每一个参数.

      1
      2
      3
      for i in "$@"; do
      echo $i
      done
  • shift 命令

    • 可以改变脚本的参数,每次都会移除当前的第一个参数 $1,移除后剩余参数依次填充,原来的 $2 变成 $1

    • 结合 while 实现另一类的循环

      1
      2
      3
      4
      5
      while [ "$1" != "" ]; do
      echo "剩下 $# 个参数"
      echo "参数:$1"
      shift
      done
    • sheift number: 一次性移除多个参数

  • getopts 命令

    • 可以解析复杂的脚本命令行参数,通常与 while 连用,取出带 - 的参数.

    • getopts optstring name: 第一个参数是字符串,就是 - 前面的字符.例如需要解析 -l -a 两个配置,且 -a 是带参数值的.optstring 要写成 la:,顺序不重要,带参数值的需要带 :.第二个参数是变量,- 后面的值就存在 name 对应的变量中.

    • 这是原教程的一个示例

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      while getopts 'lha:' OPTION; do
      case "$OPTION" in
      l)
      echo "linuxconfig"
      ;;

      h)
      echo "h stands for h"
      ;;

      a)
      avalue="$OPTARG"
      echo "The value provided is $OPTARG"
      ;;
      ?)
      echo "script usage: $(basename $0) [-l] [-h] [-a somevalue]" >&2
      exit 1
      ;;
      esac
      done
      shift "$(($OPTIND - 1))"
  • 配置项参数终止符 --

    • 指定变量只作为实体参数,而不是作为配置项参数.
  • exit 命令

    • 终止脚本执行,返回一个值.
    • exit bumber: 返回 0 即执行成功.
  • 命令执行结果

    • 脚本大多是命令的连续执行,$? 代表了前一个命令的返回值,返回值为 0 则,上一个命令执行成功.
    • 一般配合 if.
    • 或者上文中的 && ||.
  • source 命令

    • source 可以用来执行脚本,但是 source 只会在当前 shell 下执行,不需要 export.
    • . .bashrc: source 简写成 ..
  • 别名 alias 命令

    • alias NAME=DEFINITION: 别名,不表.
    • unalias lt: 解除别名.

Read 命令

  • 读取转化用户输入的命令.

  • read [-options] [variable…]: options 是参数,variable 是一系列的变量名,储存用户的输入,如果没有变量名,默认使用 REPLY 环境变量保存.

  • read FN LN: read 可以接受多个用户输入,空格分割开,输入不够 read 的数量,缺失自动为空,超过数量,都挤到最后一个变量上.

  • read 亦可以读取文件,done 后面的命令 < 将文件导入变量 myline,一次读取一行.

    1
    2
    3
    4
    while read myline
    do
    echo "$myline"
    done < $filename
  • -t second: 设置超时,超时后用户没有输入即放弃等待.

  • -p "text": 显示提示语句,提示用户输入.

  • -a xx: 将用户的输入赋值给数组,从 0 开始.

  • -n number xxx: 只取 xxx 的前 nubmber 个字符.

  • -e xx: 允许参数使用 bash 的自动补全功能.

  • -r: raw 模式,不将反斜杠解释为转义字符.

  • -s: 不显示用户输入,常用在输密码什么的.

  • IFS 变量

    • 默认 read 读取的不同值以空格隔离.
    • 可以修改 IFS 变量,修改默认的分隔符.
    • IFS 默认有空格 TAB 换行.
    • 常用自定义分隔符有 ; : 等.
    • IFS 为空,则将整行作为一个变量.

条件判断

if 是最常用的了

1
2
3
4
5
6
7
8
if commands
then
commands
[elif commands; then
commands...]
[else
commands]
fi
  • 特殊一点的是分支是 elif 结束时是 fi 结尾.
  • then 与条件判断写在一行要以 ; 分隔.其他也类似,如果愿意可以都写在一行,然后以 ; 分隔.
  • 两个特殊指令 true false.
  • 判断部分可以跟随任意多个命令,但是判断只以最后一个为准.

判断部分常用 test,有 3 种

1
2
3
4
5
6
7
8
# 写法一
test expression

# 写法二
[ expression ]

# 写法三
[[ expression ]]
  • 三种形式等价.执行成功返回 0 ,失败返回 1.
  • 形式 2 3,[] 之间必须要有空格.
  • 形式 3 还支持正则.

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 写法一
if test -e /tmp/foo.txt ; then
echo "Found foo.txt"
fi

# 写法二
if [ -e /tmp/foo.txt ] ; then
echo "Found foo.txt"
fi

# 写法三
if [[ -e /tmp/foo.txt ]] ; then
echo "Found foo.txt"
fi

判断表达式,test 是测试命令是否执行成功,测试表达式则更多的是判断状态.(这一块内容太多照抄了..)

文件判断

  • 示例 if [ -e "$FILE" ]; then
  • [ -a file ]:如果 file 存在,则为 true
  • [ -b file ]:如果 file 存在并且是一个块(设备)文件,则为 true
  • [ -c file ]:如果 file 存在并且是一个字符(设备)文件,则为 true
  • [ -d file ]:如果 file 存在并且是一个目录,则为 true
  • [ -e file ]:如果 file 存在,则为 true
  • [ -f file ]:如果 file 存在并且是一个普通文件,则为 true
  • [ -g file ]:如果 file 存在并且设置了组 ID,则为 true
  • [ -G file ]:如果 file 存在并且属于有效的组 ID,则为 true
  • [ -h file ]:如果 file 存在并且是符号链接,则为 true
  • [ -k file ]:如果 file 存在并且设置了它的 “sticky bit”,则为 true
  • [ -L file ]:如果 file 存在并且是一个符号链接,则为 true
  • [ -N file ]:如果 file 存在并且自上次读取后已被修改,则为 true
  • [ -O file ]:如果 file 存在并且属于有效的用户 ID,则为 true
  • [ -p file ]:如果 file 存在并且是一个命名管道,则为 true
  • [ -r file ]:如果 file 存在并且可读(当前用户有可读权限),则为 true
  • [ -s file ]:如果 file 存在且其长度大于零,则为 true
  • [ -S file ]:如果 file 存在且是一个网络 socket,则为 true
  • [ -t fd ]:如果 fd 是一个文件描述符,并且重定向到终端,则为 true。 这可以用来判断是否重定向了标准输入/输出错误。
  • [ -u file ]:如果 file 存在并且设置了 setuid 位,则为 true
  • [ -w file ]:如果 file 存在并且可写(当前用户拥有可写权限),则为 true
  • [ -x file ]:如果 file 存在并且可执行(有效用户有执行/搜索权限),则为 true
  • [ file1 -nt file2 ]:如果 FILE1 比 FILE2 的更新时间最近,或者 FILE1 存在而 FILE2 不存在,则为 true
  • [ file1 -ot file2 ]:如果 FILE1 比 FILE2 的更新时间更旧,或者 FILE2 存在而 FILE1 不存在,则为 true
  • [ FILE1 -ef FILE2 ]:如果 FILE1 和 FILE2 引用相同的设备和 inode 编号,则为 true

字符串判断

  • 示例 if [ -z "$ANSWER" ]; then
  • test 命令内部的>和<,必须用引号引起来(或者是用反斜杠转义)。否则,它们会被 shell 解释为重定向操作符
  • [ string ]:如果 string 不为空(长度大于 0),则判断为真。
  • [ -n string ]:如果字符串 string 的长度大于零,则判断为真。
  • [ -z string ]:如果字符串 string 的长度为零,则判断为真。
  • [ string1 = string2 ]:如果 string1string2 相同,则判断为真。
  • [ string1 == string2 ] 等同于 [ string1 = string2 ]
  • [ string1 != string2 ]:如果 string1string2 不相同,则判断为真。
  • [ string1 '>' string2 ]:如果按照字典顺序 string1 排列在 string2 之后,则判断为真。
  • [ string1 '<' string2 ]:如果按照字典顺序 string1 排列在 string2 之前,则判断为真。

整数判断

  • 示例: if [ -z "$INT" ]; then
  • [ integer1 -eq integer2 ]:如果 integer1 等于 integer2,则为 true
  • [ integer1 -ne integer2 ]:如果 integer1 不等于 integer2,则为 true
  • [ integer1 -le integer2 ]:如果 integer1 小于或等于 integer2,则为 true
  • [ integer1 -lt integer2 ]:如果 integer1 小于 integer2,则为 true
  • [ integer1 -ge integer2 ]:如果 integer1 大于或等于 integer2,则为 true
  • [ integer1 -gt integer2 ]:如果 integer1 大于 integer2,则为 true

正则判断 (最头疼的)

1
2
[[ string1 =~ regex ]]
[[ "$INT" =~ ^-?[0-9]+$ ]] # 示例

test 判断的逻辑运算

  • AND: $$-a
  • OR: ||-o
  • NOT: !
  • test 命令内部使用的圆括号,必须使用引号或者转义,否则会被 Bash 解释。

算术判断

1
2
3
if ((3 > 2)); then
echo "true"
fi

算术判断非 0 值为 true, 0 值为 false.

算术判断还可以用于赋值操作,两者都对 foo 进行了赋值,但是第一个返回 5 第二个返回 0,所以第一个判断为 true,第二个判断为 false.

1
2
if (( foo = 5 ));then echo "foo is $foo"; fi
if (( foo = 0 ));then echo "It is true.";else echo "It is false."; fi

普通命令的逻辑运算
实际上就是 if 与 && || 的混用.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
[[ -d "$dir_name" ]] && cd "$dir_name" && rm * # 等同于查看文件夹是否存在,存在则进入并执行 rm *

# 等价于
if [[ ! -d "$dir_name" ]]; then
echo "No such directory: '$dir_name'" >&2
exit 1
fi
if ! cd "$dir_name"; then
echo "Cannot cd to '$dir_name'" >&2
exit 1
fi
if ! rm *; then
echo "File deletion failed. Check results" >&2
exit 1
fi

case 结构..类似 kotlin 的 when,能避免 if 的地狱.

1
2
3
4
5
6
7
8
9
case expression in
pattern )
commands ;;
pattern )
commands ;;
...
* )
commands
esac
  • expression 是一个表达式,pattern 是表达式的值或者一个模式,可以有多条,用来匹配多个值,每条以两个分号 ; 结尾.
  • 最后是 * 如果都没匹配上.

匹配模式,可以使用各种通配符

  • a): 匹配 a
  • a|b): 匹配 a b
  • [[:alpha:]]): 单个字母
  • ???): 3 个字符的单词
  • *.txt): xxx.txt
  • *): 任意输入

bash 4.0 增加了多项匹配,即允许匹配多个条件,要以 ;;& 结束.

1
2
3
4
5
case $REPLY in
[[:upper:]]) echo "'$REPLY' is upper case." ;;&
[[:lower:]]) echo "'$REPLY' is lower case." ;;&
[[:alpha:]]) echo "'$REPLY' is alphabetic." ;;&
esac

只要条件符合,会匹配 3 个条件,而不是匹配一个就结束.

循环

总共有 3 种循环 for while until

while 和其他编程语言一样,条件为 true 就一直执行.

1
2
3
4
5
6
7
8
9
10
while condition
do
commands
done

while condition; do
commands
done

while true; do commands; done
  • 上面 3 种完全等效
  • while 条件部分,可以有任意条指令,判断只取决于最后一条.
  • 条件同样可以使用 test 命令.

until 有点不一样,与 while 相反,false 才执行,true 中断循环.

1
2
3
until condition; do
commands
done
  • 3 种方式与 while 相同,不加赘述.
  • 条件也与 while 完全相同.
  • until 有点没必要…

for .. in 常用于遍历.

1
2
3
4
for variable in list
do
commands
done
  • do 写在同一行 ;.整个循环写在一行,不再赘述.
  • 列表可以是通配符 for i in *.png; do.
  • 列表可以通过子命令产生 for i in $(cat ~/.bash_profile); do
  • in list 部分可以省略,但是非必要时,为了可读性,不要省略.

for 循环..跟 c 一样..

1
2
3
for (( expression1; expression2; expression3 )); do
commands
done
  • 定义等与 c 相同,不加赘述.
  • 循环条件是双括号.
  • 双括号内使用变量,不需要 $.

break continue..定义与 c 相同.

select 结构
结构与 for..in 基本相同,用来生成一个菜单,用户可以选择.

1
2
3
4
5
select name
[in list]
do
commands
done
  • select 会生成 list 的带编号菜单,用户选择编号,对应内容存放在 name 的变量中.
  • case 连用,当个完整的选择菜单了.

函数

bash 的函数定义上更多的是重复使用代码段的封装,而不是传统意义上的函数.

  • 函数执行不会新建 shell 这是与脚本的区别.
  • 重名下优先级 别名 > 函数 > 脚本
  • 注意函数并不依附于脚本,可以直接定义在 shell 中.
1
2
3
4
5
6
7
8
9
# 第一种
fn() {
# codes
}

# 第二种
function fn() {
# codes
}
  • 两者等价
  • 最大的不适应是变量,没有所谓变量一说.跟脚本处理变量一样
    • $0 是函数所在的脚本名.前 9 个是 $1~`$9,10 个往上就是${10}`.
    • $# 参数总数.$@ 所有参数,空格隔开.
    • $* 所有参数,以 IFS 的第一个字符隔开.

return 命令,这个 return 可是和普通函数的 return 是两码事.因为 bash 函数不带参数变量,所以 return 的返回要使用 $? 拿到.本质上和上一个命令的返回没区别.

全局变量也是个坑,如果仅仅在函数中定义 foo=1,这个 foo 是整个脚本的..想获得局部变量的效果要 local foo ; foo = 1,这一点有点坑..

数组

与一般编程语言的定义有很大不同.

  • 索引从 0 开始,没啥不一样.
  • 但是数量没有上限
  • 也没有连续索引的要求.

创建数组

逐个赋值给

1
ARRAY[INDEX]=value

一次性赋值

1
2
3
ARRAY=(value1 value2 ... valueN)
# 只赋值某些位置也可以,不要求索引连续.其他位置默认为 空字符串
names=(hatter [5]=duchess alice)

通配符

1
mp3s=( *.mp3 )

用户输入

1
read -a dice

读取数组

单个元素

1
2
# 大括号不可少
echo ${array[i]}

遍历数组,@*.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
# 访问全部元素
${foo[@]}
${foo[*]}

# 没太理解为啥
$ activities=( swimming "water skiing" canoeing "white-water rafting" surfing )
$ for act in ${activities[@]}; \
do \
echo "Activity: $act"; \
done

Activity: swimming
Activity: water
Activity: skiing
Activity: canoeing
Activity: white-water
Activity: rafting
Activity: surfing

$ for act in "${activities[@]}"; \
do \
echo "Activity: $act"; \
done

Activity: swimming
Activity: water skiing
Activity: canoeing
Activity: white-water rafting
Activity: surfing

# * 号带上双引号全部变成单个字符返回.
$ echo "${activities[*]}"

swimming water skiing canoeing white-water rafting surfing

默认位置是 0.

数组长度

1
2
3
4
5
${#array[*]}
${#array[@]}

# 但是不能拿来读取数组具体成员
echo ${#a[100]} # 数组索引 100 对应元素的长度

数组序号: {!array[@]}{!array[*]} 返回数组非空的索引.

1
2
3
for i in ${!arr[@]};do
echo ${arr[i]}
done

提取数组成员,类似切片.{array[@]:position:length}

1
2
3
echo ${food[@]:1:3}
# 从 4 开始到最后
echo ${food[@]:4}

追加, += 追加到

1
foo+=(d e f)

删除成员

  • unset 彻底抹除
  • 单纯将值置为空,只会在输出值的遍历中 “ 隐身 “,在索引中依然存在.
1
2
3
4
5
6
7
8
9
10
11
$ foo=(a b c d e f)
$ foo[1]=''
$ echo ${foo[@]}
a c d e f
$ echo ${#foo[@]}
6
$ echo ${!foo[@]}
0 1 2 3 4 5

# 直接干掉整个数组.
$ unset foo

关联数组

允许使用字符串作为索引而不是数字,有点像字典.

  • declare -A 声明数组
  • 其他几乎相同
1
2
3
4
declare -A colors
colors["red"]="#ff0000"

echo ${colors["red"]}

Set 命令

set 命令是用来修改子 Shell 环境的运行参数的,在调试等极为重要.

set -uset -o nounset: 当遇到变量不存在时,报错终止运行.(默认是忽略错误)

set -x / set +x: 脚本执行时每执行一个命令,先输出到控制台,再执行.-x 是开启 +x 关闭,一般对易出错代码段开启.

错误处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 有错误,command 会返回非 0 值
command || exit 1

# 命令失败也会继续执行
command || true

# 有错误,但是停止前需要一些操作.
command || { echo "command failed"; exit 1; }

if ! command; then echo "command failed"; exit 1; fi

command
if [ "$?" -ne 0 ]; then echo "command failed"; exit 1; fi

# 只要第一个命令执行成功,才执行第二个.
command1 && command2

set -e / set +e: 有错误即终止运行,-e 开启,+e 关闭.

set -o pipefail: -e 不适用于管道的命令,-o pipefail 适用.

连用重要

1
2
3
4
5
6
7
8
# 写法一
set -euxo pipefail
# 写法二
set -eux
set -o pipefail

# 执行时处理
bash -euxo pipefail script.sh

Shopt 命令

与 set 命令类似,但是 set 来自 POSIX,shopt 是 bash 特有的.

shopt: 可以查看所有参数及其状态.

  • -s: 打开参数
  • -u: 关闭参数
  • -q: 查询参数是否打开,但是结果不直接返回,而在 $? 中.

脚本除错

-x: 执行脚本前加上 -x 会输出所有执行的命令.

一些环境 bianl

  • LINENO: 返回所在的行号
  • FUNCNAME: 返回数组,是当前函数的调用堆栈.
  • BASH_SOURCE: 返回数组,是当前脚本的调用堆栈.
  • BASH_LINENO: 返回数组,内容是每一轮调用对应的行号.
  • ${BASH_LINENO[$i]}${FUNCNAME[$i]} 是一一对应关系,表示 ${FUNCNAME[$i]} 在调用它的脚本文件 ${BASH_SOURCE[$i+1]} 里面的行号.

Mktemp 命令

一般临时文件创建在 /tmp 下,用完就删.但是这样存在几个问题.

  • /tmp 下对所有用户可读可写.
  • 意外退出,没法清理.

mktemp: 创建时不会检查临时文件,但是支持唯一文件名和删除机制.

1
2
3
4
5
# 指定脚本退出时清理
trap 'rm -f "$TMPFILE"' EXIT

TMPFILE=$(mktemp) || exit 1
echo "Our temp file is $TMPFILE"

mktemp 会随机创建一个文件,并返回文件路径,这个文件只有当前用户拥有权限.

其他参数

  • -d: 创建临时文件夹
  • -p: 指定临时文件创建位置,默认是 $TMPDIR 下,$TMPDIR 不存在则使用 /tmp
  • -t: 可指定临时文件名的模板.末尾必须是 XXX 表示随机字符.默认是 tmp.XXXXXXXXXX

Trap 命令

trap 可以用来响应系统信号,再执行命令.

trap -l: 可以查看所有支持的系统信号.

常见格式是 trap [动作] [信号1] [信号2].

1
2
3
4
5
6
7
8
9
10
trap 'rm -f "$TMPFILE"' EXIT

# 响应执行多条命令
function egress {
command1
command2
command3
}

trap egress EXIT