├── LICENSE ├── README.md └── translator-notes.md /LICENSE: -------------------------------------------------------------------------------- 1 | CC0 1.0 Universal 2 | 3 | Statement of Purpose 4 | 5 | The laws of most jurisdictions throughout the world automatically confer 6 | exclusive Copyright and Related Rights (defined below) upon the creator and 7 | subsequent owner(s) (each and all, an "owner") of an original work of 8 | authorship and/or a database (each, a "Work"). 9 | 10 | Certain owners wish to permanently relinquish those rights to a Work for the 11 | purpose of contributing to a commons of creative, cultural and scientific 12 | works ("Commons") that the public can reliably and without fear of later 13 | claims of infringement build upon, modify, incorporate in other works, reuse 14 | and redistribute as freely as possible in any form whatsoever and for any 15 | purposes, including without limitation commercial purposes. These owners may 16 | contribute to the Commons to promote the ideal of a free culture and the 17 | further production of creative, cultural and scientific works, or to gain 18 | reputation or greater distribution for their Work in part through the use and 19 | efforts of others. 20 | 21 | For these and/or other purposes and motivations, and without any expectation 22 | of additional consideration or compensation, the person associating CC0 with a 23 | Work (the "Affirmer"), to the extent that he or she is an owner of Copyright 24 | and Related Rights in the Work, voluntarily elects to apply CC0 to the Work 25 | and publicly distribute the Work under its terms, with knowledge of his or her 26 | Copyright and Related Rights in the Work and the meaning and intended legal 27 | effect of CC0 on those rights. 28 | 29 | 1. Copyright and Related Rights. A Work made available under CC0 may be 30 | protected by copyright and related or neighboring rights ("Copyright and 31 | Related Rights"). Copyright and Related Rights include, but are not limited 32 | to, the following: 33 | 34 | i. the right to reproduce, adapt, distribute, perform, display, communicate, 35 | and translate a Work; 36 | 37 | ii. moral rights retained by the original author(s) and/or performer(s); 38 | 39 | iii. publicity and privacy rights pertaining to a person's image or likeness 40 | depicted in a Work; 41 | 42 | iv. rights protecting against unfair competition in regards to a Work, 43 | subject to the limitations in paragraph 4(a), below; 44 | 45 | v. rights protecting the extraction, dissemination, use and reuse of data in 46 | a Work; 47 | 48 | vi. database rights (such as those arising under Directive 96/9/EC of the 49 | European Parliament and of the Council of 11 March 1996 on the legal 50 | protection of databases, and under any national implementation thereof, 51 | including any amended or successor version of such directive); and 52 | 53 | vii. other similar, equivalent or corresponding rights throughout the world 54 | based on applicable law or treaty, and any national implementations thereof. 55 | 56 | 2. Waiver. To the greatest extent permitted by, but not in contravention of, 57 | applicable law, Affirmer hereby overtly, fully, permanently, irrevocably and 58 | unconditionally waives, abandons, and surrenders all of Affirmer's Copyright 59 | and Related Rights and associated claims and causes of action, whether now 60 | known or unknown (including existing as well as future claims and causes of 61 | action), in the Work (i) in all territories worldwide, (ii) for the maximum 62 | duration provided by applicable law or treaty (including future time 63 | extensions), (iii) in any current or future medium and for any number of 64 | copies, and (iv) for any purpose whatsoever, including without limitation 65 | commercial, advertising or promotional purposes (the "Waiver"). Affirmer makes 66 | the Waiver for the benefit of each member of the public at large and to the 67 | detriment of Affirmer's heirs and successors, fully intending that such Waiver 68 | shall not be subject to revocation, rescission, cancellation, termination, or 69 | any other legal or equitable action to disrupt the quiet enjoyment of the Work 70 | by the public as contemplated by Affirmer's express Statement of Purpose. 71 | 72 | 3. Public License Fallback. Should any part of the Waiver for any reason be 73 | judged legally invalid or ineffective under applicable law, then the Waiver 74 | shall be preserved to the maximum extent permitted taking into account 75 | Affirmer's express Statement of Purpose. In addition, to the extent the Waiver 76 | is so judged Affirmer hereby grants to each affected person a royalty-free, 77 | non transferable, non sublicensable, non exclusive, irrevocable and 78 | unconditional license to exercise Affirmer's Copyright and Related Rights in 79 | the Work (i) in all territories worldwide, (ii) for the maximum duration 80 | provided by applicable law or treaty (including future time extensions), (iii) 81 | in any current or future medium and for any number of copies, and (iv) for any 82 | purpose whatsoever, including without limitation commercial, advertising or 83 | promotional purposes (the "License"). The License shall be deemed effective as 84 | of the date CC0 was applied by Affirmer to the Work. Should any part of the 85 | License for any reason be judged legally invalid or ineffective under 86 | applicable law, such partial invalidity or ineffectiveness shall not 87 | invalidate the remainder of the License, and in such case Affirmer hereby 88 | affirms that he or she will not (i) exercise any of his or her remaining 89 | Copyright and Related Rights in the Work or (ii) assert any associated claims 90 | and causes of action with respect to the Work, in either case contrary to 91 | Affirmer's express Statement of Purpose. 92 | 93 | 4. Limitations and Disclaimers. 94 | 95 | a. No trademark or patent rights held by Affirmer are waived, abandoned, 96 | surrendered, licensed or otherwise affected by this document. 97 | 98 | b. Affirmer offers the Work as-is and makes no representations or warranties 99 | of any kind concerning the Work, express, implied, statutory or otherwise, 100 | including without limitation warranties of title, merchantability, fitness 101 | for a particular purpose, non infringement, or the absence of latent or 102 | other defects, accuracy, or the present or absence of errors, whether or not 103 | discoverable, all to the greatest extent permissible under applicable law. 104 | 105 | c. Affirmer disclaims responsibility for clearing rights of other persons 106 | that may apply to the Work or any use thereof, including without limitation 107 | any person's Copyright and Related Rights in the Work. Further, Affirmer 108 | disclaims responsibility for obtaining any necessary consents, permissions 109 | or other rights required for any use of the Work. 110 | 111 | d. Affirmer understands and acknowledges that Creative Commons is not a 112 | party to this document and has no duty or obligation with respect to this 113 | CC0 or use of the Work. 114 | 115 | For more information, please see 116 | 117 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # bash-handbook-zh-CN 2 | 3 | 谨以此文档献给那些想学习Bash又无需钻研太深的人。 4 | 5 | > **Tip**: 不妨尝试 [**learnyoubash**](https://git.io/learnyoubash) — 一个基于本文档的交互式学习平台! 6 | 7 | # 目录 8 | 9 | - [前言](#前言) 10 | - [Shells与模式](#shells与模式) 11 | - [交互模式](#交互模式) 12 | - [非交互模式](#非交互模式) 13 | - [返回值](#返回值) 14 | - [注释](#注释) 15 | - [变量](#变量) 16 | - [局部变量](#局部变量) 17 | - [环境变量](#环境变量) 18 | - [位置参数](#位置参数) 19 | - [Shell扩展](#shell扩展) 20 | - [大括号扩展](#大括号扩展) 21 | - [命令置换](#命令置换) 22 | - [算数扩展](#算数扩展) 23 | - [单引号和双引号](#单引号和双引号) 24 | - [数组](#数组) 25 | - [数组声明](#数组声明) 26 | - [数组扩展](#数组扩展) 27 | - [数组切片](#数组切片) 28 | - [向数组中添加元素](#向数组中添加元素) 29 | - [从数组中删除元素](#从数组中删除元素) 30 | - [流,管道以及序列](#流管道以及序列) 31 | - [流](#流) 32 | - [管道](#管道) 33 | - [命令序列](#命令序列) 34 | - [条件语句](#条件语句) 35 | - [基元和组合表达式](#基元和组合表达式) 36 | - [使用`if`](#使用if) 37 | - [使用`case`](#使用case) 38 | - [循环](#循环) 39 | - [`for`循环](#for循环) 40 | - [`while`循环](#while循环) 41 | - [`until`循环](#until循环) 42 | - [`select`循环](#select循环) 43 | - [循环控制](#循环控制) 44 | - [函数](#函数) 45 | - [Debugging](#debugging) 46 | - [后记](#后记) 47 | - [其它资源](#其它资源) 48 | - [License](#license) 49 | 50 | # 前言 51 | 52 | 如果你是一个程序员,时间的价值想必心中有数。持续优化工作流是你最重要的工作之一。 53 | 54 | 在通往高效和高生产力的路上,我们经常不得不做一些重复的劳动,比如: 55 | 56 | * 对屏幕截图,并把截图上传到服务器上 57 | * 处理各种各种的文本 58 | * 在不同格式之间转换文件 59 | * 格式化一个程序的输出 60 | 61 | 就让**Bash**来拯救我们吧。 62 | 63 | Bash是一个Unix Shell,作为[Bourne shell](https://en.wikipedia.org/wiki/Bourne_shell)的free software替代品,由[Brian Fox][]为GNU项目编写。它发布于1989年,在很长一段时间,Linux系统和OS X系统都把Bash作为默认的shell。 64 | 65 | [Brian Fox]: https://en.wikipedia.org/wiki/Brian_Fox_(computer_programmer) 66 | 67 | 68 | 那么,我们学习这个有着30多年历史的东西意义何在呢?答案很简单:这是当今最强大、可移植性最好的,为所有基于Unix的系统编写高效率脚本的工具之一。这就是你需要学习bash的原因。 69 | 70 | 在本文中,我会用例子来描述在bash中最重要的思想。希望这篇概略性的文章对你有帮助。 71 | 72 | # Shells与模式 73 | 74 | bash shell有交互和非交互两种模式。 75 | 76 | ## 交互模式 77 | 78 | Ubuntu用户都知道,在Ubuntu中有7个虚拟终端。 79 | 桌面环境接管了第7个虚拟终端,于是按下`Ctrl-Alt-F7`,可以进入一个操作友好的图形用户界面。 80 | 81 | 即便如此,还是可以通过`Ctrl-Alt-F1`,来打开shell。打开后,熟悉的图形用户界面消失了,一个虚拟终端展现出来。 82 | 83 | 看到形如下面的东西,说明shell处于交互模式下: 84 | 85 | user@host:~$ 86 | 87 | 接着,便可以输入一系列Unix命令,比如`ls`,`grep`,`cd`,`mkdir`,`rm`,来看它们的执行结果。 88 | 89 | 之所以把这种模式叫做交互式,是因为shell直接与用户交互。 90 | 91 | 直接使用虚拟终端其实并不是很方便。设想一下,当你想编辑一个文档,与此同时又想执行另一个命令,这种情况下,下面的虚拟终端模拟器可能更适合: 92 | 93 | - [GNOME Terminal](https://en.wikipedia.org/wiki/GNOME_Terminal) 94 | - [Terminator](https://en.wikipedia.org/wiki/Terminator_(terminal_emulator)) 95 | - [iTerm2](https://en.wikipedia.org/wiki/ITerm2) 96 | - [ConEmu](https://en.wikipedia.org/wiki/ConEmu) 97 | 98 | ## 非交互模式 99 | 100 | 在非交互模式下,shell从文件或者管道中读取命令并执行。当shell解释器执行完文件中的最后一个命令,shell进程终止,并回到父进程。 101 | 102 | 可以使用下面的命令让shell以非交互模式运行: 103 | 104 | sh /path/to/script.sh 105 | bash /path/to/script.sh 106 | 107 | 上面的例子中,`script.sh`是一个包含shell解释器可以识别并执行的命令的普通文本文件,`sh`和`bash`是shell解释器程序。你可以使用任何喜欢的编辑器创建`script.sh`(vim,nano,Sublime Text, Atom等等)。 108 | 109 | 除此之外,你还可以通过`chmod`命令给文件添加可执行的权限,来直接执行脚本文件: 110 | 111 | chmod +x /path/to/script.sh 112 | 113 | 这种方式要求脚本文件的第一行必须指明运行该脚本的程序,比如: 114 | 115 | ```bash 116 | #!/bin/bash 117 | echo "Hello, world!" 118 | ``` 119 | 如果你更喜欢用`sh`,把`#!/bin/bash`改成`#!/bin/sh`即可。`#!`被称作[shebang](https://zh.wikipedia.org/wiki/Shebang)。之后,就能这样来运行脚本了: 120 | 121 | /path/to/script.sh 122 | 123 | 上面的例子中,我们使用了一个很有用的命令`echo`来输出字符串到屏幕上。 124 | 125 | 我们还可以这样来使用shebang: 126 | 127 | ```bash 128 | #!/usr/bin/env bash 129 | echo "Hello, world!" 130 | ``` 131 | 132 | 这样做的好处是,系统会自动在`PATH`环境变量中查找你指定的程序(本例中的`bash`)。相比第一种写法,你应该尽量用这种写法,因为程序的路径是不确定的。这样写还有一个好处,操作系统的`PATH`变量有可能被配置为指向程序的另一个版本。比如,安装完新版本的`bash`,我们可能将其路径添加到`PATH`中,来“隐藏”老版本。如果直接用`#!/bin/bash`,那么系统会选择老版本的`bash`来执行脚本,如果用`#!/usr/bin/env bash`,则会使用新版本。 133 | 134 | 135 | ## 返回值 136 | 137 | 每个命令都有一个**返回值**(**返回状态**或者**退出状态**)。命令执行成功的返回值总是`0`(零值),执行失败的命令,返回一个非0值(错误码)。错误码必须是一个1到255之间的整数。 138 | 139 | 在编写脚本时,另一个很有用的命令是`exit`。这个命令被用来终止当前的执行,并把返回值交给shell。当`exit`不带任何参数时,它会终止当前脚本的执行并返回在它之前最后一个执行的命令的返回值。 140 | 141 | 一个程序运行结束后,shell将其**返回值**赋值给`$?`环境变量。因此`$?`变量通常被用来检测一个脚本执行成功与否。 142 | 143 | 与使用`exit`来结束一个脚本的执行类似,我们可以使用`return`命令来结束一个函数的执行并将**返回值**返回给调用者。当然,也可以在函数内部用`exit`,这 _不但_ 会中止函数的继续执行,_而且_ 会终止整个程序的执行。 144 | 145 | # 注释 146 | 147 | 脚本中可以包含 _注释_。注释是特殊的语句,会被`shell`解释器忽略。它们以`#`开头,到行尾结束。 148 | 149 | 一个例子: 150 | 151 | ```bash 152 | #!/bin/bash 153 | # This script will print your username. 154 | whoami 155 | ``` 156 | 157 | > **Tip**: 用注释来说明你的脚本是干什么的,以及 _为什么_ 这样写。 158 | 159 | # 变量 160 | 161 | 跟许多程序设计语言一样,你可以在bash中创建变量。 162 | 163 | Bash中没有数据类型。变量只能包含数字或者由一个或多个字符组成的字符串。你可以创建三种变量:局部变量,环境变量以及作为 _位置参数_ 的变量。 164 | 165 | ## 局部变量 166 | 167 | **局部变量** 是仅在某个脚本内部有效的变量。它们不能被其他的程序和脚本访问。 168 | 169 | 局部变量可以用`=`声明(作为一种约定,变量名、`=`、变量的值之间 **不应该** 有空格),其值可以用`$`访问到。举个例子: 170 | 171 | ```bash 172 | username="denysdovhan" # 声明变量 173 | echo $username # 输出变量的值 174 | unset username # 删除变量 175 | ``` 176 | 177 | 我们可以用`local`关键字声明属于某个函数的局部变量。这样声明的变量会在函数结束时消失。 178 | 179 | ```bash 180 | local local_var="I'm a local value" 181 | ``` 182 | 183 | ## 环境变量 184 | 185 | **环境变量** 是对当前shell会话内所有的程序或脚本都可见的变量。创建它们跟创建局部变量类似,但使用的是`export`关键字。 186 | 187 | ```bash 188 | export GLOBAL_VAR="I'm a global variable" 189 | ``` 190 | 191 | bash中有 _非常多_ 的环境变量。你会非常频繁地遇到它们,这里有一张速查表,记录了在实践中最常见的环境变量。 192 | 193 | | Variable | Description | 194 | | :----------- | :------------------------------------------------------------ | 195 | | `$HOME` | 当前用户的用户目录 | 196 | | `$PATH` | 用分号分隔的目录列表,shell会到这些目录中查找命令 | 197 | | `$PWD` | 当前工作目录 | 198 | | `$RANDOM` | 0到32767之间的整数 | 199 | | `$UID` | 数值类型,当前用户的用户ID | 200 | | `$PS1` | 主要系统输入提示符 | 201 | | `$PS2` | 次要系统输入提示符 | 202 | 203 | [这里](http://tldp.org/LDP/Bash-Beginners-Guide/html/sect_03_02.html#sect_03_02_04)有一张更全面的Bash环境变量列表。 204 | 205 | ## 位置参数 206 | 207 | **位置参数** 是在调用一个函数并传给它参数时创建的变量。下表列出了在函数中,位置参数变量和一些其它的特殊变量以及它们的意义。 208 | 209 | | Parameter | Description | 210 | | :------------- | :---------------------------------------------------------- | 211 | | `$0` | 脚本名称 | 212 | | `$1 … $9` | 第1个到第9个参数列表 | 213 | | `${10} … ${N}` | 第10个到N个参数列表 | 214 | | `$*` or `$@` | 除了`$0`外的所有位置参数 | 215 | | `$#` | 不包括`$0`在内的位置参数的个数 | 216 | | `$FUNCNAME` | 函数名称(仅在函数内部有值) | 217 | 218 | 在下面的例子中,位置参数为:`$0='./script.sh'`,`$1='foo'`,`$2='bar'`: 219 | 220 | ./script.sh foo bar 221 | 222 | 变量可以有 _默认_ 值。我们可以用如下语法来指定默认值: 223 | 224 | ```bash 225 | # 如果变量为空,赋给他们默认值 226 | : ${VAR:='default'} 227 | : ${$1:='first'} 228 | # 或者 229 | FOO=${FOO:-'default'} 230 | ``` 231 | 232 | # Shell扩展 233 | 234 | _扩展_ 发生在一行命令被分成一个个的 _记号(tokens)_ 之后。换言之,扩展是一种执行数学运算的机制,还可以用来保存命令的执行结果,等等。 235 | 236 | 感兴趣的话可以阅读[关于shell扩展的更多细节](https://www.gnu.org/software/bash/manual/bash.html#Shell-Expansions)。 237 | 238 | ## 大括号扩展 239 | 240 | 大括号扩展让生成任意的字符串成为可能。它跟 _文件名扩展_ 很类似,举个例子: 241 | 242 | ```bash 243 | echo beg{i,a,u}n # begin began begun 244 | ``` 245 | 246 | 大括号扩展还可以用来创建一个可被循环迭代的区间。 247 | 248 | ```bash 249 | echo {0..5} # 0 1 2 3 4 5 250 | echo {00..8..2} # 00 02 04 06 08 251 | ``` 252 | 253 | ## 命令置换 254 | 255 | 命令置换允许我们对一个命令求值,并将其值置换到另一个命令或者变量赋值表达式中。当一个命令被``` `` ```或`$()`包围时,命令置换将会执行。举个例子: 256 | 257 | ```bash 258 | now=`date +%T` 259 | # or 260 | now=$(date +%T) 261 | 262 | echo $now # 19:08:26 263 | ``` 264 | 265 | ## 算数扩展 266 | 267 | 在bash中,执行算数运算是非常方便的。算数表达式必须包在`$(( ))`中。算数扩展的格式为: 268 | 269 | ```bash 270 | result=$(( ((10 + 5*3) - 7) / 2 )) 271 | echo $result # 9 272 | ``` 273 | 274 | 在算数表达式中,使用变量无需带上`$`前缀: 275 | 276 | ```bash 277 | x=4 278 | y=7 279 | echo $(( x + y )) # 11 280 | echo $(( ++x + y++ )) # 12 281 | echo $(( x + y )) # 13 282 | ``` 283 | 284 | ## 单引号和双引号 285 | 286 | 单引号和双引号之间有很重要的区别。在双引号中,变量引用或者命令置换是会被展开的。在单引号中是不会的。举个例子: 287 | 288 | ```bash 289 | echo "Your home: $HOME" # Your home: /Users/ 290 | echo 'Your home: $HOME' # Your home: $HOME 291 | ``` 292 | 293 | 当局部变量和环境变量包含空格时,它们在引号中的扩展要格外注意。随便举个例子,假如我们用`echo`来输出用户的输入: 294 | 295 | ```bash 296 | INPUT="A string with strange whitespace." 297 | echo $INPUT # A string with strange whitespace. 298 | echo "$INPUT" # A string with strange whitespace. 299 | ``` 300 | 301 | 调用第一个`echo`时给了它5个单独的参数 —— $INPUT被分成了单独的词,`echo`在每个词之间打印了一个空格。第二种情况,调用`echo`时只给了它一个参数(整个$INPUT的值,包括其中的空格)。 302 | 303 | 来看一个更严肃的例子: 304 | 305 | ```bash 306 | FILE="Favorite Things.txt" 307 | cat $FILE # 尝试输出两个文件: `Favorite` 和 `Things.txt` 308 | cat "$FILE" # 输出一个文件: `Favorite Things.txt` 309 | ``` 310 | 311 | 尽管这个问题可以通过把FILE重命名成`Favorite-Things.txt`来解决,但是,假如这个值来自某个环境变量,来自一个位置参数,或者来自其它命令(`find`, `cat`, 等等)呢。因此,如果输入 *可能* 包含空格,务必要用引号把表达式包起来。 312 | 313 | # 数组 314 | 315 | 跟其它程序设计语言一样,bash中的数组变量给了你引用多个值的能力。在bash中,数组下标也是从0开始,也就是说,第一个元素的下标是0。 316 | 317 | 跟数组打交道时,要注意一个特殊的环境变量`IFS`。**IFS**,全称 **Input Field Separator**,保存了数组中元素的分隔符。它的默认值是一个空格`IFS=' '`。 318 | 319 | ## 数组声明 320 | 321 | 在bash中,可以通过简单地给数组变量的某个下标赋值来创建一个数组: 322 | 323 | ```bash 324 | fruits[0]=Apple 325 | fruits[1]=Pear 326 | fruits[2]=Plum 327 | ``` 328 | 329 | 数组变量也可以通过复合赋值的方式来创建,比如: 330 | 331 | ```bash 332 | fruits=(Apple Pear Plum) 333 | ``` 334 | 335 | ## 数组扩展 336 | 337 | 单个数组元素的扩展跟普通变量的扩展类似: 338 | 339 | ```bash 340 | echo ${fruits[1]} # Pear 341 | ``` 342 | 343 | 整个数组可以通过把数字下标换成`*`或`@`来扩展: 344 | 345 | ```bash 346 | echo ${fruits[*]} # Apple Pear Plum 347 | echo ${fruits[@]} # Apple Pear Plum 348 | ``` 349 | 350 | 上面两行有很重要(也很微妙)的区别,假设某数组元素中包含空格: 351 | 352 | ```bash 353 | fruits[0]=Apple 354 | fruits[1]="Desert fig" 355 | fruits[2]=Plum 356 | ``` 357 | 358 | 为了将数组中每个元素单独一行输出,我们用内建的`printf`命令: 359 | 360 | ```bash 361 | printf "+ %s\n" ${fruits[*]} 362 | # + Apple 363 | # + Desert 364 | # + fig 365 | # + Plum 366 | ``` 367 | 368 | 为什么`Desert`和`fig`各占了一行?尝试用引号包起来: 369 | 370 | ```bash 371 | printf "+ %s\n" "${fruits[*]}" 372 | # + Apple Desert fig Plum 373 | ``` 374 | 375 | 现在所有的元素都跑去了一行 —— 这不是我们想要的!为了解决这个痛点,`${fruits[@]}`闪亮登场: 376 | 377 | ```bash 378 | printf "+ %s\n" "${fruits[@]}" 379 | # + Apple 380 | # + Desert fig 381 | # + Plum 382 | ``` 383 | 384 | 在引号内,`${fruits[@]}`将数组中的每个元素扩展为一个单独的参数;数组元素中的空格得以保留。 385 | 386 | ## 数组切片 387 | 388 | 除此之外,可以通过 _切片_ 运算符来取出数组中的某一片元素: 389 | 390 | ```bash 391 | echo ${fruits[@]:0:2} # Apple Desert fig 392 | ``` 393 | 394 | 在上面的例子中,`${fruits[@]}`扩展为整个数组,`:0:2`取出了数组中从0开始,长度为2的元素。 395 | 396 | ## 向数组中添加元素 397 | 398 | 向数组中添加元素也非常简单。复合赋值在这里显得格外有用。我们可以这样做: 399 | 400 | ```bash 401 | fruits=(Orange "${fruits[@]}" Banana Cherry) 402 | echo ${fruits[@]} # Orange Apple Desert fig Plum Banana Cherry 403 | ``` 404 | 405 | 上面的例子中,`${fruits[@]}`扩展为整个数组,并被置换到复合赋值语句中,接着,对数组`fruits`的赋值覆盖了它原来的值。 406 | 407 | ## 从数组中删除元素 408 | 409 | 用`unset`命令来从数组中删除一个元素: 410 | 411 | ```bash 412 | unset fruits[0] 413 | echo ${fruits[@]} # Apple Desert fig Plum Banana Cherry 414 | ``` 415 | 416 | # 流,管道以及序列 417 | 418 | Bash有很强大的工具来处理程序之间的协同工作。使用流,我们能将一个程序的输出发送到另一个程序或文件,因此,我们能方便地记录日志或做一些其它我们想做的事。 419 | 420 | 管道给了我们创建传送带的机会,控制程序的执行成为可能。 421 | 422 | 学习如何使用这些强大的、高级的工具是非常非常重要的。 423 | 424 | ## 流 425 | 426 | Bash接收输入,并以字符序列或 **字符流** 的形式产生输出。这些流能被重定向到文件或另一个流中。 427 | 428 | 有三个文件描述符: 429 | 430 | | 代码 | 描述符 | 描述 | 431 | | :--: | :--------: | :------------------- | 432 | | `0` | `stdin` | 标准输入 | 433 | | `1` | `stdout` | 标准输出 | 434 | | `2` | `stderr` | 标准错误输出 | 435 | 436 | 重定向让我们可以控制一个命令的输入来自哪里,输出结果到什么地方。这些运算符在控制流的重定向时会被用到: 437 | 438 | | Operator | Description | 439 | | :------: | :------------------------------------------- | 440 | | `>` | 重定向输出 | 441 | | `&>` | 重定向输出和错误输出 | 442 | | `&>>` | 以附加的形式重定向输出和错误输出 | 443 | | `<` | 重定向输入 | 444 | | `<<` | [Here文档](http://tldp.org/LDP/abs/html/here-docs.html) 语法 | 445 | | `<<<` | [Here字符串](http://www.tldp.org/LDP/abs/html/x17837.html) | 446 | 447 | 以下是一些使用重定向的例子: 448 | 449 | ```bash 450 | # ls的结果将会被写到list.txt中 451 | ls -l > list.txt 452 | 453 | # 将输出附加到list.txt中 454 | ls -a >> list.txt 455 | 456 | # 所有的错误信息会被写到errors.txt中 457 | grep da * 2> errors.txt 458 | 459 | # 从errors.txt中读取输入 460 | less < errors.txt 461 | ``` 462 | 463 | ## 管道 464 | 465 | 我们不仅能将流重定向到文件中,还能重定向到其它程序中。**管道** 允许我们把一个程序的输出当做另一个程序的输入。 466 | 467 | 在下面的例子中,`command1`把它的输出发送给了`command2`,然后输出被传递到`command3`: 468 | 469 | command1 | command2 | command3 470 | 471 | 这样的结构被称作 **管道**。 472 | 473 | 在实际操作中,这可以用来在多个程序间依次处理数据。在下面的例子中,`ls -l`的输出被发送给了`grep`,来打印出扩展名是`.md`的文件,它的输出最终发送给了`less`: 474 | 475 | ls -l | grep .md$ | less 476 | 477 | 管道的返回值通常是管道中最后一个命令的返回值。shell会等到管道中所有的命令都结束后,才会返回一个值。如果你想让管道中任意一个命令失败后,管道就宣告失败,那么需要用下面的命令设置pipefail选项: 478 | 479 | set -o pipefail 480 | 481 | ## 命令序列 482 | 483 | 命令序列是由`;`,`&`,`&&`或者`||`运算符分隔的一个或多个管道序列。 484 | 485 | 如果一个命令以`&`结尾,shell将会在一个子shell中异步执行这个命令。换句话说,这个命令将会在后台执行。 486 | 487 | 以`;`分隔的命令将会依次执行:一个接着一个。shell会等待直到每个命令执行完。 488 | 489 | ```bash 490 | # command2 会在 command1 之后执行 491 | command1 ; command2 492 | 493 | # 等同于这种写法 494 | command1 495 | command2 496 | ``` 497 | 498 | 以`&&`和`||`分隔的命令分别叫做 _与_ 和 _或_ 序列。 499 | 500 | _与序列_ 看起来是这样的: 501 | 502 | ```bash 503 | # 当且仅当command1执行成功(返回0值)时,command2才会执行 504 | command1 && command2 505 | ``` 506 | 507 | _或序列_ 是下面这种形式: 508 | 509 | ```bash 510 | # 当且仅当command1执行失败(返回错误码)时,command2才会执行 511 | command1 || command2 512 | ``` 513 | 514 | _与_ 或 _或_ 序列的返回值是序列中最后一个执行的命令的返回值。 515 | 516 | # 条件语句 517 | 518 | 跟其它程序设计语言一样,Bash中的条件语句让我们可以决定一个操作是否被执行。结果取决于一个包在`[[ ]]`里的表达式。 519 | 520 | 条件表达式可以包含`&&`和`||`运算符,分别对应 _与_ 和 _或_ 。除此之外还有很多有用的[表达式](#基元和组合表达式)。 521 | 522 | 共有两个不同的条件表达式:`if`和`case`。 523 | 524 | ## 基元和组合表达式 525 | 526 | 由`[[ ]]`(`sh`中是`[ ]`)包起来的表达式被称作 **检测命令** 或 **基元**。这些表达式帮助我们检测一个条件的结果。在下面的表里,为了兼容`sh`,我们用的是`[ ]`。这里可以找到有关[bash中单双中括号区别](http://serverfault.com/a/52050)的答案。 527 | 528 | **跟文件系统相关:** 529 | 530 | | 基元 | 含义 | 531 | | :-----------: | :----------------------------------------------------------- | 532 | | `[ -e FILE ]` | 如果`FILE`存在 (**e**xists),为真 | 533 | | `[ -f FILE ]` | 如果`FILE`存在且为一个普通文件(**f**ile),为真 | 534 | | `[ -d FILE ]` | 如果`FILE`存在且为一个目录(**d**irectory),为真 | 535 | | `[ -s FILE ]` | 如果`FILE`存在且非空(**s**ize 大于0),为真 | 536 | | `[ -r FILE ]` | 如果`FILE`存在且有读权限(**r**eadable),为真 | 537 | | `[ -w FILE ]` | 如果`FILE`存在且有写权限(**w**ritable),为真 | 538 | | `[ -x FILE ]` | 如果`FILE`存在且有可执行权限(e**x**ecutable),为真 | 539 | | `[ -L FILE ]` | 如果`FILE`存在且为一个符号链接(**l**ink),为真 | 540 | | `[ FILE1 -nt FILE2 ]` | `FILE1`比`FILE2`新(**n**ewer **t**han) | 541 | | `[ FILE1 -ot FILE2 ]` | `FILE1`比`FILE2`旧(**o**lder **t**han) | 542 | 543 | **跟字符串相关:** 544 | 545 | | 基元 | 含义 | 546 | | :------------: | :---------------------------------------------------------- | 547 | | `[ -z STR ]` | `STR`为空(长度为0,**z**ero) | 548 | | `[ -n STR ]` | `STR`非空(长度非0,**n**on-zero) | 549 | | `[ STR1 == STR2 ]` | `STR1`和`STR2`相等 | 550 | | `[ STR1 != STR2 ]` | `STR1`和`STR2`不等 | 551 | 552 | **算数二元运算符:** 553 | 554 | | 基元 | 含义 | 555 | | :-----------------: | :----------------------------------------------------- | 556 | | `[ ARG1 -eq ARG2 ]` | `ARG1`和`ARG2`相等(**eq**ual) | 557 | | `[ ARG1 -ne ARG2 ]` | `ARG1`和`ARG2`不等(**n**ot **e**qual) | 558 | | `[ ARG1 -lt ARG2 ]` | `ARG1`小于`ARG2`(**l**ess **t**han) | 559 | | `[ ARG1 -le ARG2 ]` | `ARG1`小于等于`ARG2`(**l**ess than or **e**qual) | 560 | | `[ ARG1 -gt ARG2 ]` | `ARG1`大于`ARG2`(**g**reater **t**han) | 561 | | `[ ARG1 -ge ARG2 ]` | `ARG1`大于等于`ARG2`(**g**reater than or **e**qual) | 562 | 563 | 条件语句可以跟 **组合表达式** 配合使用: 564 | 565 | | Operation | Effect | 566 | | :------------: | :---------------------------------------------------------- | 567 | | `[ ! EXPR ]` | 如果`EXPR`为假,为真 | 568 | | `[ (EXPR) ]` | 返回`EXPR`的值 | 569 | | `[ EXPR1 -a EXPR2 ]` | 逻辑 _与_, 如果`EXPR1`和(**a**nd)`EXPR2`都为真,为真 | 570 | | `[ EXPR1 -o EXPR2 ]` | 逻辑 _或_, 如果`EXPR1`或(**o**r)`EXPR2`为真,为真 | 571 | 572 | 当然,还有很多有用的基元,在[Bash的man页面](http://www.gnu.org/software/bash/manual/html_node/Bash-Conditional-Expressions.html)能很容易找到它们。 573 | 574 | ## 使用`if` 575 | 576 | `if`在使用上跟其它语言相同。如果中括号里的表达式为真,那么`then`和`fi`之间的代码会被执行。`fi`标志着条件代码块的结束。 577 | 578 | ```bash 579 | # 写成一行 580 | if [[ 1 -eq 1 ]]; then echo "true"; fi 581 | 582 | # 写成多行 583 | if [[ 1 -eq 1 ]]; then 584 | echo "true" 585 | fi 586 | ``` 587 | 588 | 同样,我们可以使用`if..else`语句,例如: 589 | 590 | ```bash 591 | # 写成一行 592 | if [[ 2 -ne 1 ]]; then echo "true"; else echo "false"; fi 593 | 594 | # 写成多行 595 | if [[ 2 -ne 1 ]]; then 596 | echo "true" 597 | else 598 | echo "false" 599 | fi 600 | ``` 601 | 602 | 有些时候,`if..else`不能满足我们的要求。别忘了`if..elif..else`,使用起来也很方便。 603 | 604 | 看下面的例子: 605 | 606 | ```bash 607 | if [[ `uname` == "Adam" ]]; then 608 | echo "Do not eat an apple!" 609 | elif [[ `uname` == "Eva" ]]; then 610 | echo "Do not take an apple!" 611 | else 612 | echo "Apples are delicious!" 613 | fi 614 | ``` 615 | 616 | ## 使用`case` 617 | 618 | 如果你需要面对很多情况,分别要采取不同的措施,那么使用`case`会比嵌套的`if`更有用。使用`case`来解决复杂的条件判断,看起来像下面这样: 619 | 620 | ```bash 621 | case "$extension" in 622 | "jpg"|"jpeg") 623 | echo "It's image with jpeg extension." 624 | ;; 625 | "png") 626 | echo "It's image with png extension." 627 | ;; 628 | "gif") 629 | echo "Oh, it's a giphy!" 630 | ;; 631 | *) 632 | echo "Woops! It's not image!" 633 | ;; 634 | esac 635 | ``` 636 | 637 | 每种情况都是匹配了某个模式的表达式。`|`用来分割多个模式,`)`用来结束一个模式序列。第一个匹配上的模式对应的命令将会被执行。`*`代表任何不匹配以上给定模式的模式。命令块儿之间要用`;;`分隔。 638 | 639 | # 循环 640 | 641 | 循环其实不足为奇。跟其它程序设计语言一样,bash中的循环也是只要控制条件为真就一直迭代执行的代码块。 642 | 643 | Bash中有四种循环:`for`,`while`,`until`和`select`。 644 | 645 | ## `for`循环 646 | 647 | `for`与它在C语言中的姊妹非常像。看起来是这样: 648 | 649 | ```bash 650 | for arg in elem1 elem2 ... elemN 651 | do 652 | # 语句 653 | done 654 | ``` 655 | 656 | 在每次循环的过程中,`arg`依次被赋值为从`elem1`到`elemN`。这些值还可以是通配符或者[大括号扩展](#大括号扩展)。 657 | 658 | 当然,我们还可以把`for`循环写在一行,但这要求`do`之前要有一个分号,就像下面这样: 659 | 660 | ```bash 661 | for i in {1..5}; do echo $i; done 662 | ``` 663 | 664 | 还有,如果你觉得`for..in..do`对你来说有点奇怪,那么你也可以像C语言那样使用`for`,比如: 665 | 666 | ```bash 667 | for (( i = 0; i < 10; i++ )); do 668 | echo $i 669 | done 670 | ``` 671 | 672 | 当我们想对一个目录下的所有文件做同样的操作时,`for`就很方便了。举个例子,如果我们想把所有的`.bash`文件移动到`script`文件夹中,并给它们可执行权限,我们的脚本可以这样写: 673 | 674 | ```bash 675 | #!/bin/bash 676 | 677 | for FILE in $HOME/*.bash; do 678 | mv "$FILE" "${HOME}/scripts" 679 | chmod +x "${HOME}/scripts/${FILE}" 680 | done 681 | ``` 682 | 683 | ## `while`循环 684 | 685 | `while`循环检测一个条件,只要这个条件为 _真_,就执行一段命令。被检测的条件跟`if..then`中使用的[基元](#基元和组合表达式)并无二异。因此一个`while`循环看起来会是这样: 686 | 687 | ```bash 688 | while [[ condition ]] 689 | do 690 | # 语句 691 | done 692 | ``` 693 | 694 | 跟`for`循环一样,如果我们把`do`和被检测的条件写到一行,那么必须要在`do`之前加一个分号。 695 | 696 | 比如下面这个例子: 697 | 698 | ```bash 699 | #!/bin/bash 700 | 701 | # 0到9之间每个数的平方 702 | x=0 703 | while [[ $x -lt 10 ]]; do # x小于10 704 | echo $(( x * x )) 705 | x=$(( x + 1 )) # x加1 706 | done 707 | ``` 708 | 709 | ## `until`循环 710 | 711 | `until`循环跟`while`循环正好相反。它跟`while`一样也需要检测一个测试条件,但不同的是,只要该条件为 _假_ 就一直执行循环: 712 | 713 | ```bash 714 | until [[ condition ]]; do 715 | # 语句 716 | done 717 | ``` 718 | 719 | ## `select`循环 720 | 721 | `select`循环帮助我们组织一个用户菜单。它的语法几乎跟`for`循环一致: 722 | 723 | ```bash 724 | select answer in elem1 elem2 ... elemN 725 | do 726 | # 语句 727 | done 728 | ``` 729 | 730 | `select`会打印`elem1..elemN`以及它们的序列号到屏幕上,之后会提示用户输入。通常看到的是`$?`(`PS3`变量)。用户的选择结果会被保存到`answer`中。如果`answer`是一个在`1..N`之间的数字,那么`语句`会被执行,紧接着会进行下一次迭代 —— 如果不想这样的话我们可以使用`break`语句。 731 | 732 | 一个可能的实例可能会是这样: 733 | 734 | ```bash 735 | #!/bin/bash 736 | 737 | PS3="Choose the package manager: " 738 | select ITEM in bower npm gem pip 739 | do 740 | echo -n "Enter the package name: " && read PACKAGE 741 | case $ITEM in 742 | bower) bower install $PACKAGE ;; 743 | npm) npm install $PACKAGE ;; 744 | gem) gem install $PACKAGE ;; 745 | pip) pip install $PACKAGE ;; 746 | esac 747 | break # 避免无限循环 748 | done 749 | ``` 750 | 751 | 这个例子,先询问用户他想使用什么包管理器。接着,又询问了想安装什么包,最后执行安装操作。 752 | 753 | 运行这个脚本,会得到如下输出: 754 | 755 | ``` 756 | $ ./my_script 757 | 1) bower 758 | 2) npm 759 | 3) gem 760 | 4) pip 761 | Choose the package manager: 2 762 | Enter the package name: bash-handbook 763 | 764 | ``` 765 | 766 | ## 循环控制 767 | 768 | 我们会遇到想提前结束一个循环或跳过某次循环执行的情况。这些可以使用shell内建的`break`和`continue`语句来实现。它们可以在任何循环中使用。 769 | 770 | `break`语句用来提前结束当前循环。我们之前已经见过它了。 771 | 772 | `continue`语句用来跳过某次迭代。我们可以这么来用它: 773 | 774 | ```bash 775 | for (( i = 0; i < 10; i++ )); do 776 | if [[ $(( i % 2 )) -eq 0 ]]; then continue; fi 777 | echo $i 778 | done 779 | ``` 780 | 781 | 运行上面的例子,会打印出所有0到9之间的奇数。 782 | 783 | # 函数 784 | 785 | 在脚本中,我们可以定义并调用函数。跟其它程序设计语言类似,函数是一个代码块,但有所不同。 786 | 787 | bash中,函数是一个命令序列,这个命令序列组织在某个名字下面,即 _函数名_ 。调用函数跟其它语言一样,写下函数名字,函数就会被 _调用_ 。 788 | 789 | 我们可以这样声明函数: 790 | 791 | ```bash 792 | my_func () { 793 | # 语句 794 | } 795 | 796 | my_func # 调用 my_func 797 | ``` 798 | 799 | 我们必须在调用前声明函数。 800 | 801 | 函数可以接收参数并返回结果 —— 返回值。参数,在函数内部,跟[非交互式](#非交互模式)下的脚本参数处理方式相同 —— 使用[位置参数](#位置参数)。返回值可以使用`return`命令 _返回_ 。 802 | 803 | 下面这个函数接收一个名字参数,返回`0`,表示成功执行。 804 | 805 | ```bash 806 | # 带参数的函数 807 | greeting () { 808 | if [[ -n $1 ]]; then 809 | echo "Hello, $1!" 810 | else 811 | echo "Hello, unknown!" 812 | fi 813 | return 0 814 | } 815 | 816 | greeting Denys # Hello, Denys! 817 | greeting # Hello, unknown! 818 | ``` 819 | 820 | 我们之前已经介绍过[返回值](#返回值)。不带任何参数的`return`会返回最后一个执行的命令的返回值。上面的例子,`return 0`会返回一个成功表示执行的值,`0`。 821 | 822 | ## Debugging 823 | 824 | shell提供了用于debugging脚本的工具。如果我们想以debug模式运行某脚本,可以在其shebang中使用一个特殊的选项: 825 | 826 | ```bash 827 | #!/bin/bash options 828 | ``` 829 | 830 | options是一些可以改变shell行为的选项。下表是一些可能对你有用的选项: 831 | 832 | | Short | Name | Description | 833 | | :---: | :---------- | :----------------------------------------------------- | 834 | | `-f` | noglob | 禁止文件名展开(globbing) | 835 | | `-i` | interactive | 让脚本以 _交互_ 模式运行 | 836 | | `-n` | noexec | 读取命令,但不执行(语法检查) | 837 | | `-t` | — | 执行完第一条命令后退出 | 838 | | `-v` | verbose | 在执行每条命令前,向`stderr`输出该命令 | 839 | | `-x` | xtrace | 在执行每条命令前,向`stderr`输出该命令以及该命令的扩展参数 | 840 | 841 | 举个例子,如果我们在脚本中指定了`-x`例如: 842 | 843 | ```bash 844 | #!/bin/bash -x 845 | 846 | for (( i = 0; i < 3; i++ )); do 847 | echo $i 848 | done 849 | ``` 850 | 851 | 这会向`stdout`打印出变量的值和一些其它有用的信息: 852 | 853 | ``` 854 | $ ./my_script 855 | + (( i = 0 )) 856 | + (( i < 3 )) 857 | + echo 0 858 | 0 859 | + (( i++ )) 860 | + (( i < 3 )) 861 | + echo 1 862 | 1 863 | + (( i++ )) 864 | + (( i < 3 )) 865 | + echo 2 866 | 2 867 | + (( i++ )) 868 | + (( i < 3 )) 869 | ``` 870 | 871 | 有时我们需要debug脚本的一部分。这种情况下,使用`set`命令会很方便。这个命令可以启用或禁用选项。使用`-`启用选项,`+`禁用选项: 872 | 873 | ```bash 874 | #!/bin/bash 875 | 876 | echo "xtrace is turned off" 877 | set -x 878 | echo "xtrace is enabled" 879 | set +x 880 | echo "xtrace is turned off again" 881 | ``` 882 | 883 | # 后记 884 | 885 | 我希望这本小小的册子能很有趣且很有帮助。老实说,我写这本小册子是为了自己不会忘了bash的基础知识。我尽量让文字简明达意,希望你们会喜欢。 886 | 887 | 这本小册子讲述了我自己的Bash经验。它并非全面综合,因此如果你想了解更多,请运行`man bash`,从那里开始。 888 | 889 | 非常欢迎您的贡献,任何指正和问题我都非常感激。这些都可以通过创建一个[issue](#https://github.com/liushuaikobe/bash-handbook-zh-CN/issues)来进行。 890 | 891 | 感谢您的阅读! 892 | 893 | # 想了解更多? 894 | 895 | 下面是一些其它有关Bash的资料: 896 | 897 | * Bash的man页面。在Bash可以运行的众多环境中,通过运行`man bash`可以借助帮助系统`man`来显示Bash的帮助信息。有关`man`命令的更多信息,请看托管在[The Linux Information Project](http://www.linfo.org/)上的网页["The man Command"](http://www.linfo.org/man.html)。 898 | * ["Bourne-Again SHell manual"](https://www.gnu.org/software/bash/manual/),有很多可选的格式,包括HTML,Info,Tex,PDF,以及Textinfo。托管在上。截止到2016/01,它基于的是Bash的4.3版本,最后更新日期是2015/02/02。 899 | 900 | # 其它资源 901 | 902 | * [awesome-bash](https://github.com/awesome-lists/awesome-bash),是一个组织有序的有关Bash脚本以及相关资源的列表 903 | * [awesome-shell](https://github.com/alebcay/awesome-shell),另一个组织有序的shell资源列表 904 | * [bash-it](https://github.com/Bash-it/bash-it),为你日常使用,开发以及维护shell脚本和自定义命令提供了一个可靠的框架 905 | * [dotfiles.github.io](http://dotfiles.github.io/),上面有bash和其它shell的各种dotfiles集合以及shell框架的链接 906 | * [learnyoubash](https://github.com/denysdovhan/learnyoubash),帮助你编写你的第一个bash脚本 907 | * [shellcheck](https://github.com/koalaman/shellcheck),一个shell脚本的静态分析工具,既可以在网页[www.shellcheck.net](http://www.shellcheck.net/)上使用它,又可以在命令行中使用,安装教程在[koalaman/shellcheck](https://github.com/koalaman/shellcheck)的github仓库页面上 908 | 909 | 最后,Stack Overflow上[bash标签下](https://stackoverflow.com/questions/tagged/bash)有很多你可以学习的问题,当你遇到问题时,也是一个提问的好地方。 910 | 911 | # License 912 | 913 | [![CC 4.0][cc-image]][cc-url] © [Denys Dovhan](http://denysdovhan.com) 914 | 915 | [cc-url]: http://creativecommons.org/licenses/by/4.0/ 916 | [cc-image]: https://i.creativecommons.org/l/by/4.0/80x15.png 917 | 918 | [![CC 4.0][cc-image]][cc-url] © [Shuai Liu](http://blog.liushuaiko.be) For Chinese translation 919 | 920 | [cc-url]: http://creativecommons.org/licenses/by/4.0/ 921 | [cc-image]: https://i.creativecommons.org/l/by/4.0/80x15.png 922 | 923 | # [译者手记](./translator-notes.md) 924 | -------------------------------------------------------------------------------- /translator-notes.md: -------------------------------------------------------------------------------- 1 | # 译者手记 2 | 3 | 春节假期译者在北方某小镇的家中,偶然发现了[bash-handbook](https://github.com/denysdovhan/bash-handbook),一气读完,畅快淋漓。在之后的工作和学习中,用shell脚本写一些小工具时,每每有拿不准的地方也会想到这本小册子。它涵盖了shell脚本的基础知识,语言简明达意,是shell初学者很好的参考,亦可作为计算机从业人员的手边书。 4 | 5 | 遂产生了将其翻译成中文的想法。前后持续了一个月,利用业余时间和周末,终于完成。 6 | 7 | 由于译者水平一般,能力有限,此外Shell并非译者所使用的主力语言,因此疏漏之处在所难免。恳请同行通过[issue](https://github.com/liushuaikobe/bash-handbook-zh-CN/issues)的形式指正其中用词不准的地方或者达意的偏差,共同将其完善。之后译者会通过pull request的方式请求将中文版本合并到[原仓库](https://github.com/denysdovhan/bash-handbook)。 8 | 9 | 在译者翻译期间,正值中国的「房热」和自己的租房到期,周围的人三句离不开房,这时译者都会戴上耳机专心投入翻译工作来屏蔽这些干扰,并掩盖暂时买不起房的恐慌,消除找房的疲惫。 10 | 11 | > 通往牛逼的路上,风景差得让人只想说脏话,但创业者在意的是远方。 12 | 13 | 期待读者的鼓励。 14 | 15 | ![donate](http://7xjdjy.com1.z0.glb.clouddn.com/donate.png) 16 | --------------------------------------------------------------------------------