{"id":20948467,"url":"https://github.com/ajiew/headfirstbash","last_synced_at":"2026-05-19T07:36:31.421Z","repository":{"id":107895062,"uuid":"323356153","full_name":"aJIEw/HeadFirstBash","owner":"aJIEw","description":"Learn how to write bash scripts.","archived":false,"fork":false,"pushed_at":"2023-02-07T13:41:50.000Z","size":55,"stargazers_count":1,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"master","last_synced_at":"2025-03-13T04:44:04.862Z","etag":null,"topics":["bash","shell-script"],"latest_commit_sha":null,"homepage":"","language":"Shell","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/aJIEw.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2020-12-21T14:15:44.000Z","updated_at":"2023-03-04T09:07:03.000Z","dependencies_parsed_at":null,"dependency_job_id":"e19e0c7e-81f1-4abf-8973-57296bdfdc16","html_url":"https://github.com/aJIEw/HeadFirstBash","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/aJIEw/HeadFirstBash","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/aJIEw%2FHeadFirstBash","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/aJIEw%2FHeadFirstBash/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/aJIEw%2FHeadFirstBash/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/aJIEw%2FHeadFirstBash/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/aJIEw","download_url":"https://codeload.github.com/aJIEw/HeadFirstBash/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/aJIEw%2FHeadFirstBash/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":274455827,"owners_count":25288557,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","status":"online","status_checked_at":"2025-09-10T02:00:12.551Z","response_time":83,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["bash","shell-script"],"created_at":"2024-11-19T00:19:23.314Z","updated_at":"2026-05-19T07:36:26.392Z","avatar_url":"https://github.com/aJIEw.png","language":"Shell","funding_links":[],"categories":[],"sub_categories":[],"readme":"# HeadFirstBash\n\n记录下自己学习 Bash 时的笔记和练习内容。\n\n参考资料：\n\n-   [bash-guide](https://github.com/Idnan/bash-guide)\n-   [Bash 脚本教程](https://wangdoc.com/bash/index.html)\n-   [TLCL](http://linuxcommand.org/index.php) 的[中文翻译版](https://billie66.github.io/TLCL/index.html)\n-   [Advanced Bash-Scripting Guide](https://tldp.org/LDP/abs/html/index.html)\n-   [Bash Reference Manual](https://www.gnu.org/savannah-checkouts/gnu/bash/manual/bash.html)\n\n## 基础\n\n### 前置知识\n\n一个 Bash 脚本是一系列命令的集合，我们可以把一些重复的操作放到 Bash 脚本中执行，从而节省时间。\n\n#### 术语\n\n- Shell: 通常指 [Unix shell](https://en.wikipedia.org/wiki/Unix_shell)，一种命令行解释器。\n- [Bash](https://en.wikipedia.org/wiki/Bash_(Unix_shell)): 用于替代 [sh](https://en.wikipedia.org/wiki/Bourne_shell) 的最为广泛使用的 shell。\n- Bash 脚本：一种运行在 Bash 之上的脚本语言，主要用于系统管理、文件操作、运行环境配置等。\n\n#### 结构\n\nBash 脚本由 Shebang 行和脚本内容组成。\n\n```bash\n# 通过 Shebang 行指定脚本的解释器是 bash\n#!/usr/bin/env bash\n\n# 变量\nuser=$LOGNAME\n\n# 方法\nfunction sayHi() {\n    echo \"Hello, $user!\"\n}\n\n# 条件判断\nif [ -n $user ]; then\n    sayHi\nfi\n```\n\n相比较其它高级编程语言而言，[Bash](https://en.wikipedia.org/wiki/Bash_(Unix_shell)) 的语法虽然显得有点啰嗦，但是考虑到它诞生在上世纪 80 年代末（[sh](https://en.wikipedia.org/wiki/Bourne_shell) 更早，诞生于上世纪 70 年代末），至今已经有超过 30 年的历史了，所以，它能沿用到现在已经算是一个工程奇迹了 (#ﾟДﾟ)。\n\n#### 执行\n\n在执行脚本之前，需要先添加执行权限：\n\n```bash\nchmod +x *.sh\n```\n\n运行脚本需要使用全路径或者相对路径：\n\n```bash\n/path/to/script.sh\n# or\n./script.sh\n# or\nbash script.sh\n```\n\n直接在当前目录输入脚本名称是无法执行成功的，因为终端会将它当做命令去执行：\n\n```bash\nscript.sh # \"bash: arguments.sh: command not found\"\n```\n\n#### 符号\n\n##### 分号\n\n用于分割两个命令。\n\n```bash\ncommand1 ; command2\n```\n\n##### 命令的组合符\n\n```bash\nCommand1 \u0026\u0026 Command2 # 表示 Command1 运行成功后，继续运行 Command2\n\nCommand1 || Command2 # 表示 Command1 运行失败后，运行 Command2\n```\n\n#### [模式扩展](https://wangdoc.com/bash/expansion.html)\n\nShell 会先将命令中的特殊字符扩展，然后再执行命令。比如输入 `ls ~/workspace` 后，shell 首先会将 `~/workspace` 扩展成绝对路径，然后再执行 `ls` 命令。像这样的扩展主要有以下几种：\n\n- 用户目录扩展 `~`\n- 单个字符扩展 `?`\n- 任意字符扩展 `*`\n- 方括号扩展 `[]`\n- 大括号扩展 `{}`\n- 变量扩展 `$`\n- 子命令扩展 `$()`\n- 算术扩展 `(())`\n\n##### 子命令扩展\n\n子命令扩展用于将命令的结果作为返回值：\n\n```bash\necho $(date)\n```\n\n另外还有一种弃用的写法：\n\n```bash\necho `date`\n```\n\n#####  算术扩展\n\n算术扩展用于数字的算术运算：\n\n```bash\necho $((2+2))\n```\n\n同样有一种弃用的写法，不过不推荐使用：\n\n```bash\necho $[2+2]\n```\n\n算术扩展支持的运算符有：\n\n- \\+\n- \\-\n- \\*\n- /\n- %\n- \\*\\* (指数)\n- ++\n- \\-\\-\n\n数字默认使用十进制，但是 Bash 也支持其它进制：\n\n- `0`+数字：八进制数字\n- `0x`+数字：十六进制数字\n- `base#`+数字：base 进制数字\n\n算术扩展也支持位运算：\n\n- \u003c\u003c: 左移运算\n- \\\u003e\\\u003e: 右移运算 \n- \u0026: 位的「与」运算\n- |: 位的「或」运算\n- ~: 位的「否」运算\n- ^: 位的「异或」运算\n\n另外，算术扩展除了支持普通的逻辑运算（`\u003e` `\u003c` `==` `\u0026\u0026` `||` `!`）之外，还支持三元条件运算符：\n\n```bash\n# 语法\n$((expr1 ? expr2 : expr3))\n\n# 例子\n((result=2*3))\necho result: $((result \u003e 1? ++result: --result)) \n```\n\n由于 Bash 中所有变量都是字符串，所以，如果想要接受算术运算的结果，可以使用 `let` 关键字：\n\n```bash\nlet result=2+5\necho variable value: $result\n```\n\n除此之外，如果只是想要获得算术运算的结果，还可以使用 `expr` 命令：\n\n```bash\nexpr 2 + 2\n```\n\n不过，数字和运算符之间必须包含至少一个空格，而且特殊字符（如 `*` 等）需要使用 `\\` 转义。\n\n其它模式的具体用法见：[Shell-Expansions](https://www.gnu.org/savannah-checkouts/gnu/bash/manual/bash.html#Shell-Expansions)\n\n### 变量\n\nBash 中的变量有两种，一种是环境变量，另一种是本地变量。\n\n#### 环境变量\n\n分为系统定义的环境变量和用户定义的环境变量。当我们每次打开一个 shell 终端时，shell 会为我们初始化好所有的系统环境变量，如果是子 shell 的话还会继承父 shell 中的环境变量（通过 `export` 导出的环境变量）。\n\n我们可以使用 `env` 命令查看所有环境变量。另外，由于环境变量通常都是常量，所以一般使用大写。\n\n常见的系统环境变量：\n\n```bash\nBASHPID  # Bash 进程的 ID\nHISTSIZE # 保存历史命令的条数\nHOME     # 用户的主目录\nHOST     # 当前主机的名称\nIFS      # 词与词之间的分隔符，默认为空格+Tab+换行符\nLANG     # 系统语言\nLINENO   # 当前 shell 中已输入命令的条数\nLOGNAME  # 当前登录用户的用户名\nMACHTYPE # 机器类型，使用何种架构，x86/x86_64 等\nPATH     # 可执行命令的目录\nPWD      # 当前目录\nRANDOM   # 生成一个随机数\nSECONDS  # 当前 shell 从登入后到目前的秒数\nSHELL    # 当前 shell 的名字，比如 bash/zsh/fish 等\nTERM     # 终端类型名，即终端仿真器所用的协议\nUID      # 当前登录用户的 ID\nUSER     # 当前用户的用户名\n```\n\n#### 局部变量\n\n局部变量是用户在当前 shell 中定义的变量，我们在脚本中定义的变量也属于局部变量。我们可以使用 `set` 查看当前 shell 下所有的环境变量和局部变量。\n\n局部变量和环境变量的例子：\n\n```bash\n!/usr/bin/env bash\n\n# local variables\nstr=\"123\"\nNUM=456\necho $str\necho ${str}$NUM\n\n# environment variables\nexport SOME_VARIABLE=\"cool_variable\"\n```\n\n##### 变量默认值\n\n在使用变量时，存在一种根据变量是否为空动态返回值的语法：\n\n```bash\n${var:-ops} # 如果 var 值存在且不为空返回 var，否则返回 ops\n${var:+ops} # 如果 var 值存在且不为空返回 ops，否则返回 null (empty string)\n${var:=ops} # 如果 var 值存在且不为空返回 var，否则将 var 值设为 ops 并且返回 ops\n${var:?ops} # 如果 var 值存在且不为空返回 var，否则打印 \"var: ops\" 并且退出 (exit 1, stderr)\n```\n\n#### Tips\n\n- 变量区分大小写\n- Shell 中存在一些特殊变量：\n  - `$?` 表示上一个命令的退出码（通过 `exit` 命令）。通常 0 表示执行成功，1 表示执行失败，未指定时默认为 0。\n  - `$$` 表示当前 shell 所在进程的 ID。\n  - `$_` 表示上个命令的最后一个参数。\n  - `$@` 表示命令所有参数值。\n  - `$#` 表示上个命令的参数数量。\n\n### 字符串\n\nBash 中所有变量都是字符串。\n\n#### 获取长度\n\n使用 `${#var}` 获取字符串长度\n\n```bash\nstr=Love\necho $str\\'s length is ${#str}\n```\n\n#### 字符串截取\n\n使用 `${var:index:length}` 对字符串进行截取，如果长度未指定，则默认截取到字符串末尾。\n\n```bash\nstr=\"Love and Peace\"\necho ${str:0:4} #Love\necho ${str:9} #Peace\n```\n\n#### 字符串匹配和替换\n\n##### 头部匹配 \\# #\n\n从字符串头部开始匹配，如果匹配成功，删除匹配成功的部分并返回剩余的部分。\n\n有两种替换模式，最短替换和最长替换，分别表示为 `${path#pattern}` 和 `${path##pattern}`。\n\n```bash\npath=/home/root/workspace/dg.ajiew.me\necho original path: $path\necho after remove head dir: ${path#/*/}\necho after remove parent dirs: ${path##*/}\n```\n\n##### 尾部匹配 %\n\n和头部替换相似，只不过是从字符串尾部开始匹配，同样分为最短替换和最长替换。\n\n```bash\necho the parent dir: ${path%/*} \necho after remove end: ${path%%.*} \n```\n\n##### 全局匹配 /\n\n这个应该是最常用的替换模式，用于匹配任意位置的字符串，可以匹配一次或者所有符合条件的字符。\n\n```bash\necho another way to remove parent dirs: ${path/#\\/*\\//}\necho remove all dots: ${path//./} \n```\n\n注意，在使用全局匹配的时候，如果匹配内容包含 `/` 等特殊字符，需要使用 `\\` 转义。\n\n##### 字符串替换\n\n在字符串匹配的基础上，我们可以用目标字符替换匹配到的字符，语法如下：\n\n```bash\n${var/#old/new} # 头部匹配并替换\n${var/%old/new} # 尾部匹配并替换\n${var/old/new}  # 全局匹配并替换\n${var//old/new} # 全局匹配并替换所有\n```\n\n例子：\n\n```bash\necho new dir name: ${path/#\\/*\\//\\/etc/}\necho replace all dots with slashes: ${path//.//}\n```\n\n#### Tips\n\n- 使用 `${var^^}` 将字符串转换为大写，使用 `${varname,,}` 将字符串转换为小写。\n\n### 数组\n\nBash 中的数组没有长度限制，和绝大部分编程语言一样，数组的下标是从 0 开始的。\n\n创建数组的方法主要有两种，一种是通过下标来逐个定义数组中的元素：\n\n```bash\narray[0]=\"this\"\narray[1]=\"is\"\narray[2]=\"array\"\n```\n\n更常见的方法是通过圆括号来定义：\n\n```bash\narray=(\"this\" \"is\" \"array\")\n```\n\n也可以在圆括号中指定元素的下标：\n\n```bash\narray=([1]=\"is\" [0]=\"this\" [2]=\"array\")\n```\n\n#### 获取数组长度\n\n```bash\necho array length is ${#array[@]}\necho another way of showing length ${#array[*]}\n```\n\n注意 `@` 和 `*` 所表达的含义是一致的，下面的例子中也是一样。\n\n#### 获取数组下标\n\n```bash\necho \"${!array[@]}\" # or ${!array[*]}\n```\n\n#### 获取数组元素\n\n```bash\necho first item: $array # array will return the first one by default\necho second item: ${array[1]}\necho array: ${array[@]} # or use ${array[*]} to get all items\n```\n\n使用 `*` 的时候会使用 `IFS` 中的第一个字符作为数组的分割符。\n\n#### 数组截取\n\n和字符串截取类似，数组的截取也是通过下标+长度的方式：\n\n```bash\necho the first two elements: ${array[@]:0:2}\necho ${array[@]:6}\n```\n\n#### 数组修改\n\n```bash\n# 新增元素\narray+=(new element)\n\n# 删除元素\nunset array[0]\n```\n\n### 条件判断\n\n#### if 判断\n\nBash 中的 if 判断写法如下：\n\n```bash\nif condition 1; then\n    echo \"condition 1\"\nelif condition 2; then\n    echo \"condition 2\"\nelse\n    echo \"others\"\nfi\n```\n\n#### case 判断\n\ncase 判断主要用于单个值有多个分支情况的判断，这种情况下相比使用 if 写法更加清晰。\n\n```bash\ncase expression in\n    pattern )\n        commands ;;\n    pattern )\n        commands ;;\n    ...\nesac\n```\n\ncase 中的表达式多为字符串，pattern 多为字符串的匹配模式，除了一般的通配符之外，还可以使用字符类等来匹配。另外，Bash 4.0 之后还可以使用 `;;\u0026` 来匹配多个条件。\n\n#### 判断条件\n\n判断条件可以是普通的命令，如果命令返回值为 0 表示条件成立，否则不成立。\n\n```bash\nif cd folder; then\n    echo \"File exists\"\nfi\n```\n\n另外，判断条件也可以是是 test 命令，它有以下几种写法：\n\n```bash\ntest condition\n[ condition ]\n[[ condition ]]\n```\n\n前面两种没有区别，第三种也很常见，使用它的好处是可以省略双引号。\n\n```bash\n# 变量名中包含空格，如果在条件中不加引号，会被扩展成 -a $PWD/test file.sh 造成条件判断不成立\nfile=\"test file.sh\"\nif [ -a $PWD/$file ]; then # binary operator expected\n    echo file exists\nelse\n    echo file not found\nfi\n```\n\n解决方法有两个，要么添加双引号：\n\n```bash\nif [ -a \"$PWD/$file\" ]; then\n```\n\n或者使用双中括号：\n\n```bash\nif [[ -a $PWD/$file ]]; then\n```\n\n#### 判断条件类型\n\n##### 文件判断\n\n```bash\nif [ -d practice ] \u0026\u0026 cd practice; then\n    echo \"Entered folder: practice\"\nfi\n```\n\n##### 字符串判断\n\n```bash\nif [ $s1 = $s2 ]; then\n    echo \"s1 = s2\"\nfi\n```\n\n##### 整数判断\n\n```bash\nif [ $i1 -eq $i2 ]; then\n    echo \"i1 equals i2\"\nfi\n```\n\n##### 算术运算\n\n```bash\nif (( $s1 \u003e $s2 )); then\n    echo \"s1 longer\"\nelse\n    echo \"s2 longer\"\nfi\n```\n\n##### 正则判断\n\n```bash\n# 语法\n[[ string =~ regex ]]\n\n# 例子\nnum=1\nif [[ \"$num\" =~ ^-?[0-9]+$ ]]; then\n    echo \"$num is an integer.\"\nfi\n```\n\n### 循环\n\n#### while\n\n当符合判断条件时执行命令。\n\n```bash\nwhile condition; do\n    commands\ndone\n```\n\n#### until\n\n直到符合判断条件时，才退出执行命令。\n\n```bash\nuntil condition; do\n    commands\ndone\n```\n\n#### for\n\n```bash\nfor (( expression1; expression2; expression3 )); do\n  commands\ndone\n```\n\n#### for..in\n\n```bash\nfor variable in list; do\n    commands\ndone\n```\n\n#### select\n\nselect 循环是一个默认不断执行的循环，主要用于生成菜单项。\n\n```bash\nselect item in list; do\n    commands\ndone\n```\n\n#### 循环跳出和中断\n\nBash 中同样可以使用 `continue` 跳出循环以及 `break`  中断循环。\n\n\n### 函数\n\nBash 中可以使用别名（alias）封装单个命令，如果是复杂的命令则可以使用函数。\n\n完整的函数形式如下：\n\n```bash\nfunction functionName() {\n    echo \"Hello\"\n}\n```\n\n但实际中也可以使用省略的写法：\n\n```bash\n# omit parenthesis\nfunction greeting {\n    echo \"Hi\"\n}\n\n# omit keyword function\nfoo() {\n    echo \"bar\"\n}\n```\n\n我们可以使用 `unset -f` 删除一个函数：\n\n```bash\nunset -f functionName\n```\n\n查看所有已定义的函数：\n\n```bash\ndeclare -f [functionName]\n```\n\n#### 函数形参\n\nBash 中的函数的形参没有参数名，我们可以按照传入参数的顺序来访问每个参数，比如访问从第 1 到第 9 个参数使用 `$1` - `$9`，第 10 个之后的参数使用 `${n}` 的形式。\n\n- `$0`: 函数所在的脚本的文件名\n- `$#`: 参数总数\n- `$@`: 全部参数，使用空格分割\n- `$*`: 全部参数，使用 `$IFS` 中的第一个字符作为分割符\n\n#### 局部变量\n\nBash 脚本中声明的变量，无论是否在函数体内，都会被当做全局变量，如果想要声明函数体内的局部变量，需要使用 `local` 关键字：\n\n```bash\nfunction intro() {\n    local age=$((`date +%Y` - $birth_year))\n    echo \"Hi, my name is $1, and I'm $age years old.\"\n}\n```\n\n#### 函数返回\n\nBash 中使用 `return` 给函数指定返回值，也可以用于退出函数。\n\n```bash\nadd() {\n    local regex_num=^-?[0-9]+$\n    if [[ $1 =~ $regex_num \u0026\u0026 $2 =~ $regex_num ]]; then\n        return $(($1+$2))\n    fi\n}\n```\n\n#### 执行顺序\n\nBash 中函数的执行优先级高于脚本，但是低于别名。也就是说，如果函数名、脚本名、别名相同，执行顺序是：别名\u003e函数名\u003e脚本名。\n\n### 脚本参数\n\nBash 脚本的参数和函数的参数类似，也是通过 `$1` \\~ `$n` 的形式来访问。\n\n#### 改变脚本参数\n\n##### `shift`\n\n通过 `shift` 命令可以移除脚本的首个参数，并且使之后的参数都前移一位：\n\n```bash\n# 依次读取并移除参数\nwhile [[ -n $1 ]]; do\n    echo \"there're $# arguments left, the first one is $1\"\n    shift\ndone\n```\n\n#### 参数解析\n\n##### `getopts`\n\n通过该命令解析命令行参数的配置选项，比如 `ls -al` 中的 `-al`。\n\n```bash\n# 使用方式\ngetopts OPTSTRING VARNAME [ARGS...]\n```\n\n其中 `OPTSTRING` 表示当前命令所有可接收的配置选项，其中：\n\n- 单个字符，表示可选项无参数\n- 单个字符+`:`，表示可选项携带参数。\n\n例子：\n\n- `abc` 表示可选项为 `-a` 或 `-b` 或 `-c`，如果是其它字符会报错。\n- `abc:` 表示可选项为 `-a` 或 `-b` 或 `-c+参数`，如果是其它字符或者 `-c` 缺少参数都会报错。\n- `:ab:c:` 表示可选项为 `-a` 或 `-b 参数` 或 `-c 参数`，开头添加了 `:`，所以即使是不支持的选项也不会报错。\n\n另外，还可以配合以下环境变量使用：\n\n- `OPTARG`: 表示选项的参数。\n- `OPTIND`: 表示已处理的参数数量，默认为 1。\n\n#### 配置选项终止符\n\n`-` 和 `--` 开头的参数会被当做配置选项，所以，使用这两个字符开头的实参就需要使用配置选项终止符 `--`。\n\n```bash\ncat -- -f.sh | grep -- \"--hello\"\n```\n\n上面的例子中，文件名以 `-` 开头，所以需要使用配置选项终止符 `--`；搜索文件中的 `--` 开头的文字，同样需要在参数前添加 `--`。否则，`cat` 命令将会无法识别 `-f` 选项，`grep` 命令也会无法识别 `--hello` 选项。\n\n## 进阶\n\n### Debug\n\n#### 启动环境\n\n每次启动一个 shell，都会开启一个新的 session，一个 session 代表当前 shell 的运行环境。Session 可以分为 login shell 和 non-login shell。\n\n##### login shell\n\n顾名思义，就是需要用户登录的 shell session，启动时会从 `/etc/profile` 和 `~/.bash_profile` 中读取环境变量。注意，`~/.bash_profile` 也会触发读取 `~/.bashrc` 和 `/etc/bashrc`。\n\n##### non-login shell\n\n通常是被 login shell 启动的 shell，比如在当前 shell 中输入 `bash` 命令后，会启动一个新的 shell，此时就是一个 non-login shell，它会重新读取 `~/.bashrc` 和 `/etc/bashrc` 中的环境变量。\n\nnon-login shell 还有一种特殊的情况，那就是非交互的 (non-interactive)，最常见的是运行一个脚本的时候，所有的脚本都在单独启动的一个子 shell 中执行，并且在执行完毕后就立即退出该 shell。\n\n##### 查看 shell 类型\n\n输入下面的命令，如果返回值前带 `-` 说明是 login shell，否则就是 non-login shell。\n\n```bash\necho $0\n```\n\n#### 环境变量\n\n##### `BASH_SOURCE`\n\n返回一个字符串数组，包含脚本的调用堆栈（脚本名，最先被调用的在前）：\n\n```bash\nfunction f() {\n    echo BASH_SCRIPT[0]: ${BASH_SOURCE[0]} # 方法 f 所在的脚本名\n    echo BASH_SCRIPT[1]: ${BASH_SOURCE[1]} # 调用方法 f 所在的脚本名\n    echo BASH_SCRIPT[2]: ${BASH_SOURCE[2]} # 以此类推\n}\n```\n\n##### `FUNCNAME`\n\n返回一个字符串数组，包含方法的调用堆栈（方法名，最先被调用的在前）：\n\n```bash\nfunction f() {\n    echo funcname0: ${FUNCNAME[0]} # f 的方法名\n    echo funcname1: ${FUNCNAME[1]} # 调用方法 f 的方法名\n    echo funcname2: ${FUNCNAME[2]} # 以此类推\n}\n```\n\n##### `BASH_LINENO`\n\n返回一个字符串数组，包含方法调用的行号（最先被调用的方法在前）：\n\n```bash\nfunction f() {\n    echo \"method call at: ${BASH_LINENO}\"\n}\nf\n```\n\n##### `LINENO`\n\n返回脚本所在的行号：\n\n```bash\necho \"this line is at: $LINENO\"\n```\n\n#### Set 命令\n\nBash 脚本是在一个子 shell 中执行的，我们可以使用 `set` 命令调整 shell 运行环境的参数。\n\n##### `set -x`\n\n用于输出命令：\n\n```bash\nset -x\n\necho \"Hi\"\n\n# 输出：\n+ echo \"Hi\"\nHi\n```\n\n##### `set -u`\n\n遇到未设置的变量将报错，而不是默认的当做空值：\n\n```bash\nset -u\n\necho $A # A: unbound variable\n```\n\n##### `set -e`\n\n脚本出错时，停止执行。\n\n```bash\nset -e\n\nfoo\necho bar\n\n# 输出 `foo: command not found` 并退出，不再执行之后的命令\n```\n\n##### `set -o pipefail`\n\n当错误命令遇到管道命令时，管道命令总是会执行成功，所以我们可以使用该参数使得只要有一个子命令执行失败，整个管道命令也失败。\n\n##### `set -E`\n\n`-e` 参数会导致函数内的错误不会被 `trap` 捕获，所以可以使用 `-E` 纠正这一行为。\n\n##### `set -n`\n\n不运行命令，只检查语法是否正确。\n\n##### `set -f`\n\n不对通配符进行扩展。\n\n##### `set -v`\n\n打印 shell 脚本中的每一行输入（包括空白行）。\n\n##### `set -o noclobber`\n\n防止重定向运算符 `\u003e` 覆盖已经存在的文件。\n\n##### 总结\n\n一般在调试脚本时，可以开启以下参数：\n\n```bash\nset -Eeux\nset -o pipefail\nset -o noclobber\n```\n\n另外，也可以在启动脚本时指定对应的参数，比如：\n\n```bash\n# 不运行脚本，只检查是否有语法错误\nbash -n script.sh\n```\n\n### 常用命令\n\n收集一些常见命令的用法。\n\n#### 文件相关\n\n##### `mktemp`\n\n用于创建一个只有创建者可读写的临时文件，从而保证安全性。\n\n```bash\nTEMPFILE=$(mktemp)\n\necho $TEMPFILE\n```\n\n#### 其它命令\n\n##### `trap`\n\n用于响应系统信号。注意，如果你的命令行环境是 zsh，直接运行 `trap` 命令可能会无响应。\n\n查看所有系统信号：\n\n```bash\ntrap -l\n```\n\n在捕捉到系统信号后触发命令：\n\n```bash\ntrap 'echo \"Caught signal EXIT\"' EXIT\n```\n\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fajiew%2Fheadfirstbash","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fajiew%2Fheadfirstbash","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fajiew%2Fheadfirstbash/lists"}