从头到尾快速学习一遍Linux,高级工程师多年实践实战经验精华总结和实例示例,第五章:Shell 编程

从头到尾快速学习一遍Linux,高级工程师多年实践实战经验精华总结和实例示例,第五章:Shell 编程。

在这里插入图片描述

第五章 Shell 编程

Shell 是一个应用程序,充当用户界面和脚本解释器,是用户使用 Linux 系统的桥梁。同时,Shell 也是一种类似于 Python、PHP、Ruby 的脚本语言。


认识 Shell
####################################

从刚刚开始使用 Linux 时,就会有很多 Shell 的话题,那么 Shell 到底是什么呢?

Shell 是一个应用程序,充当用户界面和脚本解释器,是用户使用 Linux 系统的桥梁。它即是一个程序也是一种脚本语言。

因为 Shell 是一个程序,所以在 Linux 环境中有众多的 shell 程序,但是它们主要分为两大家族(Shell 家族之间的差异会比较大,同一家族的差异相对较小):

  • Bourne Shell 家族: sh、ksh、bash
  • C-Shell 家族: csh、tcsh

因为 Linux 系统中大部分默认使用 Bash,所以们主要以它主,介绍一下几个方面。

变量


变量是一个用来存储数据的实体,由一个变量名和值组成。在 Shell 中有两种不同类型的变量,分别是 Shell 变量和环境变量,也被称为局部变量和全局变量。

其实在 Bash 中只允许创建局部变量,也就是说每个新变量都会自动设置成局部变量。如果希望某个变量成为环境变量,必须使用 export 命令将局部变量修改为“局部+全局”变量,这可以称为将变量导出到环境中。Bash 处理变量的这种混乱形式是为了保持向后兼容,从 Bourne Shell 中继承过来的一种设计缺陷。

在 Linux 中启动一个 Shell 会创建一个进程,修改环境变量后只会对该进程及子进程有效,不会传递到父进程中。要使环境变量对所有的 Shell 起作用需要修改配置文件。

Shell 环境中包含很多的变量。虽然 Shell 环境变量根据发行版本的不同而不同,但是一般都会包含以下的环境变量:

变量名说明
SHELLShell 程序的名字
HOME用户的家目录
LANG系统语言及字符集
PAGER页输出程序的名字。这经常设置为/usr/bin/less。
PATH系统查找命令的路径(由冒号分开的目录列表)
PS1Shell 提示符
PWD当前工作目录。
TERM终端类型(终端仿真器所用的协议)
USER当前用户名

可以使用 printenv 和 env 命令查看 Shell 中的环境变量。

修改 PATH 命令搜索路径

选项


由于 Shell 的特殊性,除了在启动程序时添加选项外,还可以在程序运行时设置选项。即使用 set 命令的一种变体, set -o option 打开/关闭选项。

$ set -o
allexport      	off
braceexpand    	on
emacs          	on
errexit        	off
errtrace       	off
functrace      	off
hashall        	on
histexpand     	on
history        	on
ignoreeof      	on
interactive-comments	on
keyword        	off
monitor        	on
noclobber      	off
noexec         	off
noglob         	off
nolog          	off
notify         	off
nounset        	off
onecmd         	off
physical       	off
pipefail       	off
posix          	off
privileged     	off
verbose        	off
vi             	off
xtrace         	off

下边解释几个常用的选项:

选项含义
emacs命令行编辑器: Emacs 模式,关闭 vi 模式
vi命令行编辑器: vi 模式,关闭 Emacs 模式
history历史命令列表
ignoreeof忽略 eof 信号,即忽略 <Ctrl+D> 退出命令
monitor后台作业控制
noclobber是否允许重定向的输出替换文件

元字符


元字符(Meta Character)是指在 Shell 中具有特殊含义的字符,因为元字符是被 Shell 解释的,所以不同的 Shell 环境中元字符不一定完全相同。

简单的讲:元字符就是一些定义为特殊意义的字符。最常用的元字符如: ~ 表示
home 目录, & 在后台运行程序, \\ 转移字符等。

历史列表


在输入命令时,Shell 会将每条命令保存到历史列表中。可以使用不同的方式访问历史列表、调取历史命令或者对历史命令进行二次修改执行。简单的可以用 、 键调取上一条或下一条命令。

在历史列表中,每一条命令称为一个事件,而每个事件都有一个内部编号,称为事件编号。历史列表的功能就是它可以基于事件编号调取命令。例如用 !24 重新执行编号为 24 的命令。

每条执行过的命令都会添加到历史列表中,包括错误的命令以及 :ref:history <cmd_history> 命令本身。

可以设置 HISTSIZE 环境变量来指定历史列表的大小,即历史列表中可以存放历史命令的条目。

[Linux]$ export HISTSIZE=1000

自动补全


Shell 中可以使用 键自动补全命令、路径及文件名、环境变量等。如果有多个选择时,再次按 键,会显示所有可能匹配的文件列表。如果不能自动补全单词,那么 Shell 将发出嘀嘀声。

通常,自动补全有 5 种类型:

自动补全补全对象
文件名补全路径及文件名
命令补全内部及外部命令
变量补全变量
用户名补全系统上的用户名
主机名补全局域网上的计算机
# 命令补全,输入以下字符然后按 <Tab> 键,将自动补全 whoami
[Linux]$ whoa

# 变量补全,必须以 $ 符号开头,输入以下字符然后按<Tab>键
[Linux]$ echo $H
$HISTCMD       $HISTFILE      $HISTSIZE      $HOSTNAME      
$HISTCONTROL   $HISTFILESIZE  $HOME          $HOSTTYPE

# 用户名补全,必须以 ~ 符号开头,输入以下字符然后按 <Tab> 键
[Linux]$ echo ~gle

# 主机名补全,必须以 @ 符号开头,输入以下字符然后按 <Tab> 键
# 主机名自动补全只会补全包含在 /etc/hosts 文件中的主机名
[Linux]$ echo @gle

在 Bash 新版本中有一个叫做“可编程自动补全”工具。可编程自动补全允许用户(更可能是系统发行版提供商)添加额外的自动补全规则。一般来说,这样做是为了支持特定的应用,例如,可以为一个命令的长选项,添加自动补全。在 Ubuntu 发行版中定义了一个相当大的规则集合,可编程自动补全是通过 Shell 函数来实现的。

别名


别名允许用户只输入一个单词就运行任意一个命令或一组命令(包括命令选项和文件名)。可以将别名看作是命令的快捷方式(就像是软链接),也可以将别名看作是缩写。使用别名可以在命令行中减少输入的时间,使工作更流畅,同时增加生产率。

:ref:alias <cmd_alias> 命令用于创建临时的别名,在设置别名后,只在当前登录会话中有效。如果退出 Shell 或重启系统后,别名就会消失。如果想让别名永久生效,可以将别名定义写入配置文件 ~/.bashrc 中。不加选项和参数执行 :ref:alias <cmd_alias> 命令会显示所有已定义的别名列表。 :ref:unalias <cmd_unalias> 命令用于删除别名。

# 定义别名
[Linux]$ alias info='date; who'

# 查看系统中的别名
[Linux]$ alias 
alias info='data; who'
alias la='ls -A'
alias ll='ls -alF'
alias ls='ls --color=auto'
alias vi='vim

# 删除别名
[Linux]$ unalias info

别名的日常用法总结:

  1. 为命令设置默认的参数(例如 alias ping=‘ping -c 5’ 设置 ping 命令的次数,alias rm=‘rm -i’ 删除文件时需要确认)。
  2. 设置系统中多版本命令的默认路径(例如 GNU/grep 位于 /usr/local/bin/grep 中而 Unix grep 位于 /bin/grep 中。若想默认使用 GNU grep 则设置别名 grep=‘/usr/local/bin/grep’ )。
  3. 为跨平台的操作创建命令别名,以增加兼容性(比如 alias ipconfig=ifconfig)。

内置命令


Shell 有很多内置在其源代码中的命令。这些命令是内置的,所以 Shell 不必到磁盘上搜索它们,执行速度因此加快。不同的 Shell 内置命令有所不同。

Bash 常用的内置命令

  • :ref:alias <cmd_alias> :显示和创建已有命令的别名。

  • :ref:bg <cmd_bg> :把作业放到后台。

  • :ref:cd <cmd_cd> :改变目录,如果不带参数,则回到主目录,带参数则切换到参数所指的目录。

  • :ref:disown <cmd_disown> :从作业表中删除一个活动作业。

  • :ref:echo <cmd_echo> :显示变量或字符。

  • :ref:eval <cmd_eval> :把命令读入 Shell,并执行。

  • :ref:exec <cmd_exec> :运行命令,替换掉当前 Shell。

  • :ref:exit <cmd_exit> :以指定状态退出 Shell。

  • :ref:export <cmd_export> :使变量可被子 Shell 识别。

  • :ref:fc <cmd_fc> :历史的修改命令,用于编辑历史命令。

  • :ref:fg <cmd_fg> :把后台作业放到前台。

  • :ref:getopts <cmd_getopts> :解析并处理命令行选项。

  • :ref:help <cmd_help> :显示关于内置命令的有用信息。如果指定了一个命令,则将显示该命令的详细信息。

  • :ref:history <cmd_history> :显示带行号的命令历史列表。

  • :ref:jobs <cmd_jobs> :显示放到后台的作业。

  • :ref:kill <cmd_kill> :向指定的进程发送关闭信号。

  • :ref:logout <cmd_logout> :退出登录 Shell。

  • :ref:pwd <cmd_pwd> :打印出当前的工作目录。

  • :ref:read <cmd_read> :从标准输入读取。

  • :ref:set <cmd_set> :设置选项和位置参量。

  • :ref:stop <cmd_pid> :暂停进程的运行。

  • :ref:suspend <cmd_suspend> :终止当前 Shell 的运行(对登录 Shell 无效)。

  • :ref:times <cmd_times> :显示由当前 Shell 启动的进程所累计的用户时间和系统时间。

  • :ref:type <cmd_type> :显示命令的类型。

  • :ref:unalias <cmd_unalias> :取消所有的命令别名设置。

  • :ref:wait <cmd_wait> :等待后台进程结束,并显示它的结束状态。

    编写 Shell 脚本时,可以使用特殊的内置命令 for、if、while 等来控制脚本流程,这些命令有时候称为关键字。

配置文件


Bash 允许自定义工作环境,其中包含两类:初始化文件和注销文件,其中初始化文件又分为登陆文件和环境文件。

在这里插入图片描述

当用户登录系统时,首先自动执行系统管理员建立的全局登录配置 /ect/profile 。注意不是自动运行 bashrc,而是 profile(通常 profile 中设置了 bashrc 的执行)。

然后在用户起始目录下按顺序查找三个特殊文件中的一个: ~/.bash_profile -> ~/.bash_login -> ~/.profile ,但只执行最先找到的一个。

测试时发现新建 ~/.bash_login 文件后,不自动加载 ~/.bashrc 的情况。可以在登录脚本中加入 source ~/.bashrc 让每次登录时自动加载 ~/.bashrc 配置文件。

按功能划分,也可以分为两类:

  • profile 类:为交互式 Shell 提供配置,用于定义环境变量、用于运行命令或脚本。profile 里面的内容,在系统登录后执行。

  • bashrc 类:为非交互式和交互式 Shell 提供配置,经常用于初始化文件,定义命令别名、定义本地变量。bashrc 在登录 Shell 时会自动执行。

    Shell 可以分为交互式和非交互式两种,当正常登陆系统时启动的是交互式的 Shell,当运行脚本时启动的是非交互式的 Shell。启动不同类型的 Shell 在加载配置文件时,会有细微的差别。


Shell 元字符
####################################

在 Shell 中,许多非字母、数字字符都拥有特殊的含义,这些字符被称为元字符。其中有几个元字符用于文件名扩展,也称为通配符。先来了解一下 Shell 中的元字符都有那些。

字符含义
~通配符:home 目录
?通配符:匹配任意一个字符
*通配符:匹配 0 个或多个字符
[]通配符:匹配一组字符中的任意一个字符
{}通配符:匹配一组字符中的任意一个字符串或识别变量的边界
;在同一行中分隔多条命令
#注释
&在后台运行命令
$引用变量
\转义符
取消所有替换(强引用)
"取消部分替换(弱引用)
`命令替换
!历史列表:事件标记
<重定向输入
>重定向输出
|管道
()在子 Shell 中运行命令

通配符


通配符实际上是一种 Shell 实现的路径扩展功能。当命令中包含通配符时,Shell 会先解释通配符的意义,然后在将命令重组,最后运行该命令。

字符含义实例
*匹配 0 或多个字符a*b 匹配 aabcb、axyzb、a012b、ab …
?匹配任意一个字符a?b 匹配 aab、abb、acb、a0b …
[list]匹配 list 中的任意单一字符a[xyz]b 匹配 axb、ayb、azb
[!list]匹配除 list 中的任意单一字符a[!0-9]b 匹配 axb、aab、a-b …
[a-f]匹配 a~f 中的任意单一字符a[a-f]b 匹配 aab、abb、acb …
{abc, …}匹配大括号内的任意一个字符串a{abc,xyz,123}b 匹配 aabcb、axyzb、a123b
通配符看起来有点像正则表达式语句,但是它与正则表达式不同,不能相互混淆。把通配符理解为
Shell 特殊字符就行。而且涉及的只有 ``* ? [] {}`` 这几种。

在使用 ``[]`` 和 ``{}`` 时,字符中间不能有空格。

通配符可以用在所有的 Shell 命令中,组合起来可以起到意想不到的作用。

# 一次性创建多个文件
[Linux]$ touch number{1,2,3,4}.txt
[Linux]$ ls
number1.txt  number2.txt  number3.txt  number4.txt

[Linux]$ touch number{1..9}.txt
[Linux]$ ls
number1.txt  number3.txt  number5.txt  number7.txt  number9.txt
number2.txt  number4.txt  number6.txt  number8.txt


# 在使用命令前可以先使用 echo 命令验证结果
[Linux]$ echo {10..23}
10 11 12 13 14 15 16 17 18 19 20 21 22 23 

[Linux]$ echo {0..3}{Z..X}
0Z 0Y 0X 1Z 1Y 1X 2Z 2Y 2X 3Z 3Y 3X

[Linux]$ echo a{A{1,2},B{3,4}}b
aA1b aA2b aB3b aB4b

命令分隔符


在大多数情况下,一条命令行只需输入一条命令。可以用 ; 将两条命令连接起来,从而在一条命令行中输入多个命令。在输入时 ; 两边可以不加空格也可以加空格,为了方便阅读可以在 ; 后边加入一个空格。

[Linux]$ date; cal
Wed 19 Aug 2020 09:44:55 PM CST
    August 2020       
Su Mo Tu We Th Fr Sa  
                   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                 

[Linux]$ whoami; pwd; date
glenn
/home/glenn/Public
Wed 19 Aug 2020 09:48:32 PM CST

还有两个特殊的命令分隔符,即 &&|| ,也叫做条件执行。 && 前的命令执行成功后会继续执行后面的命令,俗称和命令。 || 前的命令执行失败后才会执行后面的命令,俗称或命令。

如果想在命令没有运行成功时,追加一条警告信息,可以使用以下命令:

    [Linux]# update || echo 'The update program failed.'

注释


注释在命令行中基本不会用到,主要应用在脚本中。注释可以出现在脚本的任意位置,在每一行中 # 字符之后的内容都会被注释掉(在脚本执行时,会忽略所有的注释)。

# 以下两种注释相等,一般注释会单独占用一行

# 显示时间及内核
[Linux]$ date; uname
Wed 19 Aug 2020 09:58:54 PM CST
Linux

[Linux]$ date; uname # 显示时间及内核
Wed 19 Aug 2020 09:58:54 PM CST
Linux

后台运行程序


在单独的 Shell 程序中,也有前后台之分。前台即实时显示的内容,后台类似于图形界面中最小化的程序。将程序(或脚本)放入后台有两种方法:

元字符 & 将程序放入后台执行

====================================

在输入命令时在后边加入 & 符号会把命令程序直接放到后台执行,此时可以用 :ref:jobs 命令 <cmd_jobs> 查看后台执行程序的列表。

# 将 tar 命令放入后台执行
[Linux]$ tar -cvf web.tar /var/www/html/ &

在后台运行命令时,有输出的命令(如:ping)一样会将结果输出到屏幕,所以最好将输出重定向到某个文件(如: ping www.baidu.com >out.file 2>&1 )。

使用 <Ctrl+Z> 快捷键暂停程序运行

====================================

使用 <Ctrl+Z> 快捷键放入后台的命令处于暂停状态,是不会运行的。

[Linux]$ top

# 使用 <Ctrl+Z> 快捷键可以将 top 放入后台
[Linux]$ jobs
[1]-  Stopped                 vi
[2]+  Stopped                 top

bg 命令和 fg 命令

在后台暂停的命令,可以使用 bg 命令让程序在后台继续执行。格式如下:

    [Linux]$ bg %1

而 fg 命令用于把后台工作恢复到前台执行,格式如下:

    [Linux]$ fg %1

在使用 bg 和 fg 命令时, %1 为后台的编号( % 可以省略)。当命令不带参数执行时会对应带有 + 号的后台程序。

引用变量


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

[Linux]$ your_name=glenn
[Linux]$ echo $your_name
glenn
[Linux]$ echo ${your_name}
glenn

变量名外面的花括号是可选的,加不加都行。加花括号是为了帮助解释器识别变量的边界,比如下面这种情况:

[Linux]$ name=Java
[Linux]$ echo ${name}Script

如果不给 name 变量加花括号,写成 echo $nameScript ,解释器就会把 $nameScript 当成一个变量(其值为空),代码执行结果就不是我们期望的样子了。

推荐给所有变量加上花括号,这是个好的编程习惯。

已定义的变量,可以被重新赋值,如:

[Linux]$ your_name=glenn
[Linux]$ echo $your_name
glenn
[Linux]$ your_name=rose
[Linux]$ echo $your_name
rose

给变量赋值时不能写成 $your_name=rose ,只有使用变量的时候才加 $ 符。

转义符


有时候,可能希望按字面上的含义使用元字符,而不使用其特殊含义。例如,将分号作为分号使用,而不是一个命令分隔符。在这种情况下就需要用转义符去转义元字符。 Shell 中有三种转义符。

=============== =================
字符 说明
=============== =================
\(反斜杠) 转义符,去除紧跟其后的元字符的特殊意义
'(单引号) 强引用,所有的元字符都使用其字面含义。强引用中不允许再次出现单引号
"(双引号) 弱引用,只保留 $ 、 `````和 \ 三种元字符的特殊含义
=============== =================

# 转义符转义
[Linux]$ echo It is warm and sunny\; come over and visit
It is warm and sunny; come over and visit

# 中间有空格的文件名使用引用
[Linux]$ cd 'your name'
[Linux]$ cd "your name"
  • 使用转义符转义单个字符
  • 使用强引用引用字符串
  • 使用弱引用引用字符串,保留 $ 、 `````和 \ 三种元字符的特殊含义。

转义字符有强弱之分, \ 大于 ' 大于 "

命令替换


命令替换允许在一条命令中嵌入另一条命令,Shell 首先执行嵌入的命令,并且用输出替换该命令,然后在执行整条命令。通过将一条命令封装在 `````中,可以将它嵌入另一条命令。

[Linux]$ echo "The time and date are `date`"
The time and date are Fri 21 Aug 2020 09:29:52 PM CST 

命令替换还有另外一种格式,即将命令放入 $() 中, $(command) 等同于 command。注意区分 ${} 是引用变量。

历史列表


Shell 通过使用 ! 字符,为历史列表提供了一个特殊的扩展功能。例如用 !24 重新执行历史列表中事件编号为 24 的命令。

Shell 中还有几个好用的历史列表命令:

字符说明
!!执行上一条命令(等同于 + 键)
!number执行历史列表中第 number 行的命令
!string执行最近的以 string 字符串开头的命令
!?string执行最近的包含这个字符串的命令
!*使用上一条命令的选项和参数
!$使用上一条命令的最后一个参数

应该谨慎地使用 !string!?string 格式,除非你完全确信历史列表条目的内容。

[Linux]$ ls -l Music/
total 0
[Linux]$ !!
ls -l Music/
total 0
[Linux]$ ls !*
ls -l Music/
total 0
[Linux]$ ls !$
ls Music/

重定向


在 Shell 中,标准输入/标准输出的概念很好理解。默认情况下,大多数程序从键盘读取输入,并将输出写入到屏幕。标准输入(stdin)默认为键盘输入;标准输出(stdout)默认为屏幕输出;标准错误输出(stderr)默认也是输出到屏幕。

在 Linux 进程中,每个输入源和每个输出目标都会有一个唯一的数字标识,这个数字称为文件描述符。例如,一个进程可以从文件 #8 中读取数据,并将数据写入到文件 #6 中。默认情况下,Linux 为每个进程提供 3 个预定义的文件描述符,即 0 代表标准输入, 1 代表标准输出, 2 代表标准错误。

类型描述符默认值系统文件
标准输入(standard input)0从键盘获取/proc/self/fd/0
标准输出(standard output)1输出到屏幕/proc/self/fd/1
错误输出(error output)2输出到屏幕/proc/self/fd/2

在 Shell 中也可以改变默认的标准输入、标准输出或错误输出,来实现输入输出的重定向。比如将标准输出指向文件时,那么标准的输出就会保存到文件中。

# 重定向输出,用标准输出替换文件中的内容
[Linux]$ date > date.txt 
[Linux]$ cat date.txt 
Sat 22 Aug 2020 09:29:27 PM CST

# 重定向输出,将标准输出的内容追加到文件中
[Linux]$ cal >> date.txt 
[Linux]$ cat date.txt 
Sat 22 Aug 2020 09:29:27 PM CST
    August 2020
Su Mo Tu We Th Fr Sa
                   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

在重定向输出时(也包括下边的重定向标准错误),如果指定的文件不存在则会新建文件。一定要小心区分替换和追加的区别,不要丢失了重要的数据。

- ``>`` 替换文件中的数据
- ``>>`` 将输出追加到文件的末尾

Shell 中定义了两种不同的输出目标:标准输出和错误输出,标准输出用于正常输出,错误输出用于错误消息输出。重定向标准错误时,需要加入错误输出文件描述符(即数字 2 ),当重定向错误输出时,不会影响标准输入和标准输出。

[Linux]$ ls a.txt b
ls: cannot access 'b': No such file or directory
a.txt
[Linux]$ ls a.txt b 2> error
a.txt
[Linux]$ cat error 
ls: cannot access 'b': No such file or directory

# 分开定义标准输出和错误输出
[Linux]$ ls a.txt b > out 2> error

# 将错误输出追加到文件中
[Linux]$ ls a.txt b 2>> error

如果想将标准输出和错误输出重定向到同一个位置,可以先重定向标准输出,然后在将错误输出指定到标准输出中。

[Linux]$ ls a.txt b > output 2>&1

除了重定向输出,还可以用 < 重定向输入。甚至同时指定重定向输入和输出。

[Linux]$ cat a.txt
a
c
d
b

# 重定向输入
[Linux]$ sort < a.txt
a
b
c
d

# 同时指定重定向输入和输出
[Linux]$ sort < a.txt > b.txt
[Linux]$ cat b.txt
a
b
c
d

在 Linux 中,有一个特殊的设备文件,即 /dev/null ,它会丢弃一切写入其中的数据(但会反馈写入操作成功)。

在程序员行话中, 将 /dev/null 称为位桶(bit bucket)或黑洞(black hole),经常被用于丢弃不需要的输出流,或作为输入流的空文件。

如果不希望看到命令的错误信息,可以将错误输出重定向到 /dev/null 中。同时可以使用 cat /dev/null > a.txt 清空一个文件中的内容。

管道


在 Linux 设计原则里,每个命令都是一个小工具,每个工具只出色的完成一件事情。当靠一个工具无法解决问题时,能够使用一组命令来完成任务。Shell 允许创建一序列命令,将一个命令的标准输出发送到下一个命令的标准输入,两个命令之间需要用 | 符号连接,这时,一序列命令称为管道线(pipeline), | 符号称为管道(pipe)。

# 将文件 a 和文件 b 的内容发送到 less 命令中读取
[Linux]$ cat a.txt b.txt | less

上边的命令中,less 只处理 cat 的正确输出结果,如果文件 b.txt 不存在,则只会显示 a.txt 文件的内容。可以将 cat 命令的标准输出和错误输出一起发送给 less 命令,命令为 cat a.txt b.txt 2>&1 | less

可以将管道想象成真实的水管,在污水处理中,污水(输出)从一端进入,经过一层过滤(命令)之后流向另一层再去过滤,直到污水被净化干净。

有时候,可能希望将程序的输出同时发送到两个地方。例如,希望将一个输出即保存在文件中,同时还发送到另一个程序。这时可以使用 tee 命令,tee 命令会从标准输入读取数据,将其内容输出到标准输出,同时保存成文件。命令语法为:

command 1 | tee file | command2

乍看起来,管道也有重定向的作用,它也改变了数据输入输出的方向,那么,管道和重定向之间到底有什么不同呢?

简单地说,重定向将命令与文件连接起来,用文件来接收命令的输出;而管道将命令与命令连接起来,用第二个命令来接收第一个命令的输出。来看一个特殊的例子:

    # 注意这里是 root 用户
    [Linux]# cd /usr/bin
    [Linux]# ls > less

第一条命令将当前目录切换到了大多数程序所存放的目录,第二条命令是告诉 Shell 用 ls 命令的输出覆盖文件 less 中的内容。因为 /usr/bin 目录已经包含了 less(程序)文件,所以重定向会破坏西系统中的 less 程序。

这是使用重定向错误重写文件的一个教训,所以在使用时要谨慎。

在子 Shell 运行命令


当在 Shell 中执行命令时,首先 Shell 会判断这条命令是内部命令(Shell 中内置的命令)还是外部命令(单独的程序)。如果是内部命令就直接解释命令,如果是外部命令会创建一个 Shell 副本进程(即子 Shell)运行这个程序。当程序终止时,会重新将控制权交还给原 Shell (即父 Shell),并等待输入另一条命令。

将命令用小括号括起来执行,括号中的命令将会新开一个子 Shell 顺序执行,可以在括号中用 ; 组合多条命令,圆括号中的命令被称为一个编组。

[Linux]$ (cd ./file; ./a.py)

当用 ssh 远程登陆主机执行,因为所有的命令都是 ssh 进程的子进程,所以当 ssh 断开连接时,所有的命令都会被杀死。如果想在断开连接时,命令依然在后台执行,可以将命令放入 () 中执行。

    [Linux]$ (ping www.baidu.com > /dev/null &)
    [Linux]$ ps -ef | grep ping
    glenn     22202     1  0 21:22 pts/0    00:00:00 ping www.baidu.com
    glenn    22204 21736  0 21:22 pts/0    00:00:00 grep ping

可以看到进程的父 ID 是 init 而不是当前终端的进程 ID,因而关闭 ssh 连接后无任何影响。


Shell 快捷键
####################################

命令行用户在使用命令行时并不喜欢敲入太多文本,所以命令中才会有 cp、ls、mv 和 rm 那么多缩写的命令名。命令行最大的目标之一就是减少操作(省事),用最少的击键次数执行最多的任务。为了提高命令行下的工作效率,使用一些快捷键是提高效率的最简单也是最直接方式。

Bash 使用了一个名为 Readline 的库(供不同的应用程序共享使用的线程集合),来实现命令行编辑。例如:用箭头键来移动光标,此外还有许多特性。

Shell 中有两种命令行编辑的模式,即 vi 模式和 Emacs 模式(Bash 默认使用的是 Emacs 模式)。在两种模式中使用不同的快捷键操纵在命令行中键入的内容,包括使用历史列表和自动补全的功能。

如果快捷键不能使用,可以将命令行编辑模式设置为 Emacs 默认值。

    [Linux]$ set -o emacs

控制 Shell 进程


========== ==========
按键 作用
========== ==========
挂起终端(有点像卡住了,但输入的命令还会执行)
恢复挂起的终端(恢复输入状态,并输出挂起时执行的命令)
终止前台命令的执行
发送表示标准输入结束的 EOF 信号。经常用于退出程序或 Shell。
暂停前台命令的执行,并放入后台保存
========== ==========

移动光标


========== ==========
按键 作用
========== ==========
上一条命令(相当于 )
下一条命令(相当于 )
光标左移一个字符(相当于 )
光标右移一个字符(相当于 )
移动光标到行首
移动光标到行尾
========== ==========

剪切和粘贴


Readline 的文档使用术语 killing 和 yanking 来指代我们平常所说的剪切和粘贴。剪切的本文存储在一个叫做剪切环(kill-ring)的缓冲区中。

========== ==========
按键 作用
========== ==========
删除一个字符(相当于 )
剪切从光标处到行首的字符
剪切从光标处到行尾的字符
剪切从光标处到词尾的字符
粘贴文本到光标处
清空行(如果命令是从历史列表中复制的,则会恢复到原始命令)
========== ==========

其它常用功能


========== ==========
按键 作用
========== ==========
清空屏幕(相当于 clear 命令)
反向递增搜索历史列表中的命令
换行符(相当于 )
返回符(相当于 )
插入控制字符
互换光标处和光标前面的字符位置
========== ==========

在开发 Unix 时因为成本的原因,使用了电传打字机作为终端。在电传打字机上执行回车时,包含两种操作,首先将托盘返回到最左边的位置上(即 CR 码返回信号),然后将打印纸向上移动一行(即 LF 码换行信号)。

在终端快捷键中, 发送返回信号, 发送换行信号。在终端执行命令时,软件会把 CR 和 LF 信号转换为 CR+LF 回车信号。

自定义快捷键


使用 bind 命令可以自定义快捷键,Bash 中的快捷键其实是 Readline 来提供的,因此,这里快捷键的设置其实就是配置 Readline,Readline 中分两种快捷键,一种是 Readline 内部的函数快捷键,另外一种是执行 Shell 命令,设置的时候稍有不同:

# 查看 Readline 中可以使用的函数名称
bind -l

# 已经绑定的快捷键
bind -p

# 自定义快捷键
# 绑定后,按[C-x,C-L]就能执行ls -al
bind -x '"/C-x/C-l":ls -al'

这种设置只针对当前的会话有效,一旦会话丢失,设置的快捷键就会丢失。为了让设置的快捷键永久有效,需要编辑配置文件,在 Linux 系统中,有两个配置文件(全局的和用户的),全局的配置文件是 /etc/inputrc ,而用户的配置文件在家目录下 ~/.inputrc 。inputrc 文件的大概样子像下面这样:

# 本例来自 CentOS6.4 
$if mode=emacs

# for linux console and RH/Debian xterm
"/e[1~": beginning-of-line
"/e[4~": end-of-line
# commented out keymappings for pgup/pgdown to reach begin/end of history
#"/e[5~": beginning-of-history
#"/e[6~": end-of-history
"/e[5~": history-search-backward
"/e[6~": history-search-forward
"/e[3~": delete-char
"/e[2~": quoted-insert
"/e[5C": forward-word
"/e[5D": backward-word
"/e[1;5C": forward-word
"/e[1;5D": backward-word

# for rxvt
"/e[8~": end-of-line
"/eOc": forward-word
"/eOd": backward-word

# for non RH/Debian xterm, can't hurt for RH/DEbian xterm
"/eOH": beginning-of-line
"/eOF": end-of-line

# for freebsd console
"/e[H": beginning-of-line
"/e[F": end-of-line
$endif

在配置文件中,/C 代表 键,/M 代表 键,/e 代表 键,// 代表反斜杠,/' 代表单引号,/" 代表双引号;

可以通过 来查看某一个功能键的字符序列,或者输入 cat 后回车,进入编辑中,直接按快捷键。配置文件中可能会使用八进制或者十六进制来表示字符。


Shell 脚本之始
############################

Shell 脚本是一种以 Shell 程序为解释器的脚本程序。由于习惯的原因,业界所说的“Shell 编程”都是指编写 Shell 脚本,不是指 Shell 程序扩展开发。

Linux 操作系统本身就像是一个 API,所以 Shell 编程跟常见的解释型语言(Python、PHP、Ruby)一样,只要有一个文本编辑器和一个脚本解释器就可以了。

Hello world


在终端窗口新建一个 hello.sh 脚本文件(扩展名并不影响脚本执行)。输入以下内容:

[Linux]$ echo "#!/bin/bash" > hello.sh
[Linux]$ echo "echo 'hello world' " >> hello.sh

运行脚本:

[Linux]$ chmod +x hello.sh
[Linux]$ ./hello.sh
hello world

不要惊讶,这就是一个 shell 脚本了。

在执行脚本时一定要写成 ./hello.sh ,直接写 hello.sh ,Linux 系统会去系统变量 PATH 里寻找有没有 test.sh 的命令,要用 ./test.sh 告诉系统,就在当前目录找。


Shell 编程语法
####################################

先来看个例子吧:

#!/bin/bash
cd ~
mkdir shell_tut
cd shell_tut
    
for ((i=0; i<10; i++)); do
    touch test_$i.txt
done
  • 第1行:指定脚本解释器,这里是用 Bash 做解释器的
  • 第2行:切换到当前用户的 home 目录
  • 第3行:创建一个目录 shell_tut
  • 第4行:切换到 shell_tut 目录
  • 第5行:循环条件,一共循环 10 次
  • 第6行:创建 test_0…9.txt 文件
  • 第7行:循环体结束

mkdir, touch 都是系统自带的程序,一般在 /bin 或者 /usr/bin 目录下。for, do, done 是 Shell 脚本语言的关键字。

运行 Shell 脚本有两种方法:

作为可执行程序

chmod +x test.sh
./test.sh

注意,一定要写成 ./test.sh,而不是 test.sh,运行其它二进制的程序也一样,直接写 test.sh,linux 系统会去 PATH 里寻找有没有叫 test.sh 的,而只有 /bin, /sbin, /usr/bin,/usr/sbin 等在 PATH 里,你的当前目录通常不在 PATH 里,所以写成 test.sh 是会找不到命令的,要用 ./test.sh 告诉系统说,就在当前目录找。

通过这种方式运行 bash 脚本,第一行一定要写对,好让系统查找到正确的解释器。

这里的"系统",其实就是 shell 这个应用程序(想象一下 Windows Explorer),但我故意写成系统,是方便理解,既然这个系统就是指 shell,那么一个使用 /bin/sh 作为解释器的脚本是不是可以省去第一行呢?是的。

作为解释器参数

这种运行方式是,直接运行解释器,其参数就是 shell 脚本的文件名,如:

/bin/sh test.sh
/bin/php test.php

这种方式运行的脚本,不需要在第一行指定解释器信息,写了也没用。

变量


定义变量

定义变量时,变量名不加美元符号($),如:

your_name="qinjx"

注意,变量名和等号之间不能有空格,这可能和你熟悉的所有编程语言都不一样。

除了显式地直接赋值,还可以用语句给变量赋值,如:

for file in `ls /etc`

使用变量

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

your_name="qinjx"
echo $your_name
echo ${your_name}

变量名外面的花括号是可选的,加不加都行,加花括号是为了帮助解释器识别变量的边界,比如下面这种情况:

for skill in Ada Coffe Action Java; do
	echo "I am good at ${skill}Script"
done

如果不给 skill 变量加花括号,写成 echo “I am good at $skillScript”,解释器就会把 $skillScript 当成一个变量(其值为空),代码执行结果就不是我们期望的样子了。

推荐给所有变量加上花括号,这是个好的编程习惯。IntelliJ IDEA 编写 shell script 时,IDE 就会提示加花括号。

重定义变量

已定义的变量,可以被重新定义,如:

your_name="qinjx"
echo $your_name

your_name="alibaba"
echo $your_name

这样写是合法的,但注意,第二次赋值的时候不能写 $your_name=“alibaba”,使用变量的时候才加美元符。

注释


以“#”开头的行就是注释,会被解释器忽略。

多行注释

sh 里没有多行注释,只能每一行加一个 # 号。就像这样:

#--------------------------------------------
# 这是一个自动打ipa的脚本,基于webfrogs的ipa-build书写

# 功能:自动为etao ios app打包,产出物为14个渠道的ipa包
# 特色:全自动打包,不需要输入任何参数
#--------------------------------------------

##### 用户配置区 开始 #####
#
#
# 项目根目录,推荐将此脚本放在项目的根目录,这里就不用改了
# 应用名,确保和Xcode里Product下的target_name.app名字一致
#
##### 用户配置区 结束  #####

如果在开发过程中,遇到大段的代码需要临时注释起来,过一会儿又取消注释,怎么办呢?每一行加个#符号太费力了,可以把这一段要注释的代码用一对花括号括起来,定义成一个函数,没有地方调用这个函数,这块代码就不会执行,达到了和注释一样的效果。

字符串


字符串是 shell 编程中最常用最有用的数据类型(除了数字和字符串,也没啥其它类型好用了,哈哈),字符串可以用单引号,也可以用双引号,也可以不用引号。单双引号的区别跟 PHP 类似。

单引号

str='this is a string'

单引号字符串的限制:

  • 单引号里的任何字符都会原样输出,单引号字符串中的变量是无效的
  • 单引号字串中不能出现单引号(对单引号使用转义符后也不行)

双引号

your_name='qinjx'
str="Hello, I know your are \"$your_name\"! \n"
  • 双引号里可以有变量
  • 双引号里可以出现转义字符

字符串操作

拼接字符串

your_name="qinjx"
greeting="hello, "$your_name" !"
greeting_1="hello, ${your_name} !"

echo $greeting $greeting_1

获取字符串长度

string="abcd"
echo ${#string} #输出:4

提取子字符串

string="alibaba is a great company"
echo ${string:1:4} #输出:liba

查找子字符串

string="alibaba is a great company"
# 找出字母 i 的位置
echo `expr index "$string" is`
3

更多 参见本文档末尾的参考资料中Advanced Bash-Scripting Guid Chapter 10.1

数组

管道

条件判断

流程控制


和 Java、PHP 等语言不一样,sh 的流程控制不可为空,如:

<?php
if (isset($_GET["q"])) {
	search(q);
}
else {
	//do nothing
}

在 sh/bash 里可不能这么写,如果 else 分支没有语句执行,就不要写这个 else。

还要注意,sh 里的 if [ $foo -eq 0 ],这个方括号跟 Java/PHP 里 if 后面的圆括号大不相同,它是一个可执行程序(和 ls, grep 一样),想不到吧?在 CentOS 上,它在 /usr/bin 目录下:

ll /usr/bin/[
-rwxr-xr-x. 1 root root 33408 6月  22 2012 /usr/bin/[

正因为方括号在这里是一个可执行程序,方括号后面必须加空格,不能写成if [$foo -eq 0]

if else

if

if condition
then
	command1 
	command2
	...
	commandN 
fi

写成一行(适用于终端命令提示符):

if `ps -ef | grep ssh`;  then echo hello; fi

末尾的 fi 就是 if 倒过来拼写,后面还会遇到类似的

if else

if condition
then
	command1 
	command2
	...
	commandN
else
	command
fi

if else-if else

if condition1
then
	command1
elif condition2
	command2
else
	commandN
fi

for while

for

在开篇的示例里演示过了:

for var in item1 item2 ... itemN
do
	command1
	command2
	...
	commandN
done

写成一行:

for var in item1 item2 ... itemN; do command1; command2… done;

C 风格的 for

for (( EXP1; EXP2; EXP3 ))
do
	command1
	command2
	command3
done

while

while condition
do
	command
done

无限循环

while :
do
	command
done

或者

while true
do
	command
done

或者

for (( ; ; ))

until

until condition
do
	command
done

case

case "${opt}" in
	"Install-Puppet-Server" )
		install_master $1
		exit
	;;

	"Install-Puppet-Client" )
		install_client $1
		exit
	;;

	"Config-Puppet-Server" )
		config_puppet_master
		exit
	;;

	"Config-Puppet-Client" )
		config_puppet_client
		exit
	;;

	"Exit" )
		exit
	;;

	* ) echo "Bad option, please choose again"
esac

case 的语法和 C family 语言差别很大,它需要一个 esac(就是 case 反过来)作为结束标记,每个 case 分支用右圆括号,用两个分号表示 break

函数

定义

调用

文件包含


可以使用 source 和 . 关键字,如:

source ./function.sh
. ./function.sh

在 bash 里,source 和 . 是等效的,他们都是读入 function.sh 的内容并执行其内容(类似 PHP 里的 include),为了更好的可移植性,推荐使用第二种写法。

包含一个文件和执行一个文件一样,也要写这个文件的路径,不能光写文件名,比如上述例子中:

. ./function.sh

不可以写作:

. function.sh

如果 function.sh 是用户传入的参数,如何获得它的绝对路径呢?方法是:

real_path=`readlink -f $1`#$1是用户输入的参数,如function.sh
. $real_path

用户输入

执行脚本时传入

脚本运行中输入

select菜单

stdin和stdout

参考资料


  • Advanced Bash-Scripting Guide <http://tldp.org/LDP/abs/html/>_ 非常详细,非常易读,大量 example,既可以当入门教材,也可以当做工具书查阅
  • Unix Shell Programming <http://www.tutorialspoint.com/unix/unix-shell.htm_
  • Linux Shell Scripting Tutorial - A Beginner's handbook <http://bash.cyberciti.biz/guide/Main_Page>_

开箱即用的 Shell 脚本
####################################

脚本实例


检测两台服务器目录的一致性
+++++++++++++++++++++++++++++++++++

通过对比文件或目录的 md5 值,来检测两台服务器指定目录下的文件是否一致。

#!/bin/bash
######################################
检测两台服务器指定目录下的文件一致性
#####################################
# 通过对比两台服务器上文件的 md5 值,达到检测一致性的目的
dir=/data/web
b_ip=192.168.88.10
# 将指定目录下的文件遍历出来并作为 md5sum 命令的参数
# 进而得到所有文件的 md5 值,并写入到指定文件中
find $dir -type f|xargs md5sum > /tmp/md5_a.txt
ssh $b_ip "find $dir -type f|xargs md5sum > /tmp/md5_b.txt"
scp $b_ip:/tmp/md5_b.txt /tmp
#将文件名作为遍历对象进行一一比对
for f in `awk '{print 2} /tmp/md5_a.txt'`do
#以 a 机器为标准,当 b 机器不存在遍历对象中的文件时直接输出不存在的结果
if grep -qw "$f" /tmp/md5_b.txt
then
md5_a=`grep -w "$f" /tmp/md5_a.txt|awk '{print 1}'`
md5_b=`grep -w "$f" /tmp/md5_b.txt|awk '{print 1}'`
#当文件存在时,如果 md5 值不一致则输出文件改变的结果
if [ $md5_a != $md5_b ]then
echo "$f changed."
fi
else
echo "$f deleted."
fi
done

定时清空文件
+++++++++++++++++++++++++++++++++++

通过计划任务使脚本每小时执行一次,当时间为 0 点或 12 点时,将目标目录下的所有文件内容清空,但不删除文件,其他时间则只统计各个文件的大小,一个文件一行,输出到以时间和日期命名的文件中,需要考虑目标目录下二级、三级等子目录的文件

#!/bin/bash
#################################################################
定时清空文件,记录文件大小
################################################################
logfile=/tmp/`date +%H-%F`.log
n=`date +%H`
if [ $n -eq 00 ] || [ $n -eq 12 ]
then
# 通过 for 循环遍历目标目录下的所有文件并做相应操作
for i in `find /data/log/ -type f`
do
true > $i
done
else
for i in `find /data/log/ -type f`
do
du -sh $i >> $logfile
done
fi

检测网卡流量
+++++++++++++++++++++++++++++++++++

检测网卡流量,并且每分钟记录一次。

#!/bin/bash
#######################################################
#检测网卡流量,并按规定格式记录在日志中规定一分钟记录一次
#日志格式如下所示:
#2019-08-12 20:40
#ens33 input: 1234bps
#ens33 output: 1235bps
######################################################3
while :
do
# 设置语言为英文,保障输出结果是英文,否则会出现 bug
LANG=en
logfile=/tmp/`date +%d`.log
# 将下面执行的命令结果输出重定向到 logfile 日志中
exec >> $logfile
date +"%F %H:%M"
# sar 命令统计的流量单位为 kb/s ,日志格式为 bps,因此要 *1000*8
sar -n DEV 1 59|grep Average|grep ens33|awk '{print $2,"\t","input:","\t",$5*1000*8,"bps","\n",$2,"\t","output:","\t",$6*1000*8,"bps"}'
echo "####################"
# 因为执行 sar 命令需要 59 秒,因此不需要 sleep
done

计算文档每行的数字个数
+++++++++++++++++++++++++++++++++++

计算文档每行出现的数字个数,并计算整个文档的数字总数

#!/bin/bash
#########################################################
#计算文档每行出现的数字个数,并计算整个文档的数字总数
########################################################
# 使用 awk 只输出文档行数(截取第一段)
n=`wc -l a.txt|awk '{print $1}'`
sum=0
# 文档中每一行可能存在空格,因此不能直接用文档内容进行遍历
for i in `seq 1 $n`do
# 输出的行用变量表示时,需要用双引号
line=`sed -n "$i"p a.txt`#wc -L选项,统计最长行的长度
n_n=`echo $line|sed s'/[^0-9]//'g|wc -L`
echo $n_nsum=$[$sum+$n_n]
done
echo "sum:$sum"

统计 .html 文件总大
+++++++++++++++++++++++++++++++++++

统计目录中以 .html 结尾的文件的总大小。

# 方法 1:
find . -name "*.html" -exec du -k {} \; |awk '{sum+=$1}END{print sum}'

# 方法 2:
for size in $(ls -l *.html |awk '{print $5}'); do
    sum=$(($sum+$size))
done
echo $sum

扫描主机端口状态
+++++++++++++++++++++++++++++++++++

扫描本地主机的端口状态

#!/bin/bash
#########################################################
# 扫描本地主机的端口状态
########################################################
HOST=$1
PORT="22 25 80 8080"
for PORT in $PORT; do
    if echo &>/dev/null > /dev/tcp/$HOST/$PORT; then
        echo "$PORT open"
    else
        echo "$PORT close"
    fi
done

批量修改服务器用户密码
++++++++++++++++++++++++++++++++

Linux 主机 SSH 连接信息:旧密码

# 查看旧密码文件,内容格式:IP User Password Port
# cat old_pass.txt 
192.168.18.217  root    123456     22
192.168.18.218  root    123456     22

SSH远程修改密码脚本:新密码随机生成
#!/bin/bash
OLD_INFO=old_pass.txt
NEW_INFO=new_pass.txt
for IP in $(awk '/^[^#]/{print $1}' $OLD_INFO); do
    USER=$(awk -v I=$IP 'I==$1{print $2}' $OLD_INFO)
    PASS=$(awk -v I=$IP 'I==$1{print $3}' $OLD_INFO)
    PORT=$(awk -v I=$IP 'I==$1{print $4}' $OLD_INFO)
    # 随机密码
    NEW_PASS=$(mkpasswd -l 8)  
    echo "$IP   $USER   $NEW_PASS   $PORT" >> $NEW_INFO
    expect -c "
    spawn ssh -p$PORT $USER@$IP
    set timeout 2
    expect {
        \"(yes/no)\" {send \"yes\r\";exp_continue}
        \"password:\" {send \"$PASS\r\";exp_continue}
        \"$USER@*\" {send \"echo \'$NEW_PASS\' |passwd --stdin $USER\r exit\r\";exp_continue}
    }"
done

# 生成新密码文件
# cat new_pass.txt 
192.168.18.217  root    n8wX3mU%      22
192.168.18.218  root    c87;ZnnL      22

根据 web 日志,封禁异常 IP
++++++++++++++++++++++++++++++++

根据 web 访问日志,封禁请求量异常的 IP,如 IP 在半小时后恢复正常,则解除封禁。

#!/bin/bash
#####################################################################
#根据 web 日志,封禁异常 IP,如 IP 在半小时后恢复正常,则解除封禁
#####################################################################
logfile=/data/log/access.log
#显示一分钟前的小时和分钟
d1=`date -d "-1 minute" +%H%M`
d2=`date +%M`
ipt=/sbin/iptables
ips=/tmp/ips.txt
block()
{ 
#将一分钟前的日志全部过滤出来并提取IP以及统计访问次数
 grep '$d1:' $logfile|awk '{print $1}'|sort -n|uniq -c|sort -n > $ips
 #利用for循环将次数超过100的IP依次遍历出来并予以封禁
 for i in `awk '$1>100 {print $2}' $ips` 
 do
 $ipt -I INPUT -p tcp --dport 80 -s $i -j REJECT 
 echo "`date +%F-%T` $i" >> /tmp/badip.log 
 done
}
unblock()
{ 
#将封禁后所产生的pkts数量小于10的IP依次遍历予以解封
 for a in `$ipt -nvL INPUT --line-numbers |grep '0.0.0.0/0'|awk '$2<10 {print $1}'|sort -nr` 
 do 
 $ipt -D INPUT $a
 done
 $ipt -Z
}
#当时间在00分以及30分时执行解封函数
if [ $d2 -eq "00" ] || [ $d2 -eq "30" ] 
 then
 #要先解再封,因为刚刚封禁时产生的pkts数量很少
 unblock
 block 
 else
 block
fi

  • 23
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

代码讲故事

你的鼓励将是我创作的最大动力

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

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

打赏作者

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

抵扣说明:

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

余额充值