1.实验目的与要求
- Shell特殊字符的使用
- Shell变量和运算符
- Shell输入输出语句
- Shell函数定义和调用
2.实验平台
实验室安装的实验环境(Linux操作系统)和头歌(www.educoder.net)实验平台(课程实验)
3.实验内容
- 练习Shell特殊字符的使用
- 练习Shell变量和字符串
- 练习Shell函数的使用
4.实验详细内容、步骤
任务描述
本关任务:欢迎来到shell
脚本世界,为了见证shell
脚本的神奇,本小节带领大家写第一个shell脚本,希望脚本能够在右侧的命令行窗口中输出hello EduCoder
。
接下来让我们一起去开启shell
脚本的快乐之旅吧!
相关知识
Shell是一个用 C 语言编写的程序,它是用户使用 Linux
的桥梁。Shell
既是一种命令语言,又是一种程序设计语言。为了更好地学习shell
编程,您需要对linux
系统命令有一定的了解,熟悉常用的命令,如:vim
、echo、chmod
等。
shell 语言编写的程序通常都非常轻巧,比如我想编写一个在屏幕上显示Hello World !
程序:
- #!/bin/bash
- echo "Hello World !"
说明:#!
是一个约定的标记,它告诉系统这个脚本需要什么解释器来执行,即使用哪一种 shell
。echo
命令用于向窗口输出文本内容。
Shell 脚本的运行通常有两种方法: 1、作为可执行程序运行
- chmod +x ./test.sh #使脚本具有执行权限
- ./test.sh #执行脚本
注意:一定要写成 ./test.sh
,而不是 test.sh
,./
的作用其实就是告诉系统就在当前目录查找,否则系统会去PATH
路径中查找有没有test.sh
的文件,而系统默认情况下只有 /bin
, /sbin
, /usr/bin
,/usr/sbin
等在 PATH
中,很难找到。
2、作为解释器参数运行,这种运行方式是直接运行解释器,其参数就是 shell 脚本的文件名,如:
- /bin/sh test.sh
这种方式运行的脚本,不需要在第一行指定解释器信息。
编程要求
请按照要求,完成以下实践内容。 1、在/opt/
目录下创建第一个shell
脚本 文件命名为 test.sh
; 2、编写test.sh
脚本,让其输出Hello EduCoder
(通过 vim 编辑器编辑); 3、给/opt/test.sh
赋予执行权限; 4、运行test.sh
文件。
任务描述
本关带领大家熟悉 shell 的变量并掌握其使用。
变量概念
从字面上来看就是可以变的量,举个例子,我们小时候都做过数学的应用题,经常定义 y 的值是某个数,如果换了一道题,还是定义 y 的值,但是 y 的值就不和第一道题相同了,这个 y 就是变量。
变量是计算机内存的单元,在shell中变量其中存放的值可以改变。当 Shell 脚本需要保存一些信息时,如一段字符串或者一段数据,我们可以通过变量赋值的方式把他保存在变量中。
每个变量都有一个名字,所以命令变量,尽量做到,见名知义。 那么,在 shell 中应该如何设置变量呢?其实 so easy,命令如下:
- [root@localhost ~]# name=jerry
- #定义变量name的值
- [root@localhost ~]# echo $name
- jerry
- #查询变量的值
变量类型
运行 shell 时,会同时存在三种变量:
- 局部变量: 局部变量在脚本或命令中定义,仅在当前 shell 实例中有效,其他 shell 启动的程序不能访问局部变量,通常用于函数本地。
- i. #local 关键字
- ii. local VAR_NAME=VALUE
- 环境变量: 所有的程序,包括 shell 启动的程序,都能访问环境变量,有些程序需要环境变量来保证其正常运行。必要的时候 shell 脚本也可以定义环境变量 。
- i. ##对当前shell进程及其子shell有效,对其它的shell进程无效
- ii. 定义:export VAR_NAME=VALUE
- iii. 导出:export VAR_NAME
- iv. 撤消变量:unset VAR_NAME
- v. 只读变量:readonly VAR_NAME
- shell变量: shell变量是由 shell 程序设置的特殊变量。shell 变量中有一部分是环境变量,有一部分是局部变量,这些变量保证了 shell 的正常运行 位置变量:用来接受变量指定位置的参数
- i. $1,$2...,${10}
- ii. ## 如下执行脚本a.sh
- iii. bash a.sh 1 2 3
- iv. ## $1即为第一个参数1 $2及为第二个参数 2
特殊变量:shell 对一些参数做特殊处理,这些参数只能被引用而不能被赋值。
-
- v. $# 传递到脚本的参数个数
- vi. $* 显示所有向脚本传递的参数 #与位置变量不同,此选项参数可超过9个
- vii. $$ 获取当前shell的进程号
- viii. $! 执行上一个指令的进程号
- ix. $? 获取执行的上一个指令的返回值 #0为执行成功,非零为执行失败
- x. $- 显示shell使用的当前选项,与set命令功能相同
- xi. $@ 与$*相同,但是使用时加引号,并在引号中返回每个参数
变量定义规范
1)在定义变量时,有一些规则需要遵守。
变量名可以由字母、数字和下画线组成,但是不能以数字开头,不能使用程序中的关键字(保留字),要见名知义。如果变量名是"20name或者if",则是错误的。
2)在 Bash 中,变量的默认类型都是字符串型,如果要进行数值运算,则必须指定变量类型为数值型。比如:
- [root@localhost ~]# aa=1+2
- [root@localhost ~]# echo $aa
- 1+2
看到了吧,变量 aa 的值不是"3",而是"1+2"。在 Bash 中,变量类型是字符串型,所以认为"1+2"只是一个字符串,而不会进行数值运算(数值运算方法会在后续章节中介绍)。
3) 变量用等号"="连接值,"="左右两侧不能有空格。这是 Shell 语言特有的格式要求。在绝大多数的其他语言中,"="左右两侧是可以加入空格的。但是在 Shell 中命令的执行格式是"命令 [选项] [参数]",如果在"="左右两侧加入空格,那么 Linux 会误以为这是系统命令,是会报错的。
4) 变量值中如果有空格,则需要使用单引号或双引号包含,如 test="hello world!"。双引号括起来的内容"$"和反引号者都拥有特殊含义,而单引号括起来的内容都是普通字符。
5) 在变量值中,可以使用转义符"\"
。
6) 如果需要増加变量值,那么可以进行变量叠加。 例如:
- [root@localhost ~]#test=123
- #叠加变量test,变量值变成了123456
- [root@localhost ~]#test=${test}456
- [root@localhost ~]# echo $test
- 123456
7) 如果要把命令的执行结果作为变量值赋予变量,则需要使用反引号或 $() 包含命令。例如:
- ##$()引用执行结果
- [root@localhost ~]# test=$(date)
- [root@localhost ~]# echo $test
- Tue Feb 4 14:50:45 CST 2020
- ##反引号 引用执行结果
- root@Test-old-web01:~# test=`date`
- root@Test-old-web01:~# echo $test
- Tue Feb 4 14:51:19 CST 2020
8) 环境变量名建议大写,便于区分。
删除变量
使用 unset 命令可以删除变量。语法:
- unset variable_name
变量被删除后不能再次使用。unset 命令不能删除只读变量(readonly myUrl 关键字 readonly 定义的变量叫做只读变量),如下:
- #!/bin/sh
- myUrl="http://www.educoder.net"
- unset myUrl
- echo $myUrl
- ##输出为空,是因为myurl已经被删除了
实践要求
- 创建一个脚本
/opt/test2.sh
, 已知向/opt/test.sh
传递参数 "www" "educoder" "net" 三个字符串。 /opt/test.sh
需要实现如下功能:输出传递的参数的个数,并且输出第一个参数。
比如: 向 /opt/test2.sh
传递 是"a" "b" "c" "d",则执行 bash /opt/test.sh a b c d
输出的结果应该是 4 a
。
任务描述
本关带领大家熟悉 shell 的变量并掌握其使用。
相关知识
字符串概念
字符串是 shell 编程中最常用最有用的数据类型(除了数字和字符串,也没啥其它类型好用了),字符串可以用单引号,也可以用双引号,也可以不用引号。
单引号 双引号 反引号的区别
双引号 中可以有变量; 双引号里可以先转义字符;双引号中的单引号输出时维持单引号不变。请下面的案例: 例 1:
- #! /bin/bash
- var="aaa
- bbb
- ccc"
- echo ${var}
- ##输出结果为
- aaa bbb ccc
例 2:
- #! /bin/bash
- var="aaa
- bbb
- ccc"
- echo "${var}"
- ##输出结果为
- aaa
- bbb
- ccc
单引号: 会忽略所有的特殊字符,即任何字符都会原样输出,包括定义的变量; 单引号字串中不能出现单引号。
- #! /bin/bash
- var="aaa
- bbb
- ccc"
- echo '${var}'
- ##输出结果为
- ${var}
- ###单引号无法解析变量,只会原样输出
反引号 : 有命令替换的作用见例 3;反引号可以嵌套使用,但内层的单引号必须加上\
进行转义见例 4。 例 3:
- $ echo the date is `date`
- the date is Tue Feb 4 18:08:12 CST 2020
- #这里的反引号 `date` 及为命令"date" 的结果,因此像引用一个命令返回的结果作为变量用 反引号 替换,也可以使用 $(date)的方式来替换使用命令结果
例 4:
- $ abc=`echo The number of users is \`who| wc -l\``
- $ echo $abc
- The number of users is 4
- ## 这里 `who|wc -l` 是指 返回的正在登录系统的用户的个数的结果 为4
- ## 因为反引号嵌套了反引号,所以在需要加上\转义
字符串常见操作表达式
示例:
获取字符串长度
- string="abcdefg"
- echo ${#string}
字符串截取
- ${string:position} //在$string中, 从位置$position开始提取子串
- ${string:position:length} //在$string中, 从位置$position开始提取长度为$length的子串
- 测试例子
- string="abc12342341"
- echo ${string:4} //2342341 从第4位开始截取后面所有字符串
- echo ${string:3:3} //123 从第3位开始截取后面3位
- echo ${string:3:6} //123423 从第3位开始截取后面6位
- echo ${string: -4} //2341 :右边有空格 截取后4位
- echo ${string:(-4)} //2341 同上
- echo ${str:(-6):5} //34234 从倒数第6个位置向左提取5个字符字符串,
字符串匹配删除
- ${string#substring} //从变量$string的开头, 删除最短匹配$substring的子串
- ${string##substring} //从变量$string的开头, 删除最长匹配$substring的子串
- ${string%substring} //从变量$string的结尾, 删除最短匹配$substring的子串
- ${string%%substring} //从变量$string的结尾, 删除最长匹配$substring的子串
- 测试例子
- test='c:/windows/boot.ini'
- $ echo ${test#/} (从头匹配斜杠/,删除匹配到最短的斜杠,没有匹配到,所以没有删除)
- c:/windows/boot.ini
- $ echo ${test#*/} (删除 从头匹配删除匹配到最短以/结尾的字符串,*是匹配0个或者多个)
- windows/boot.ini
- $ echo ${test##*/} (删除 从头匹配匹配到最长的以/结尾的字符串,通常可以用来获取到文件名)
- boot.ini
- $ echo ${test%/*} (删除 从尾部匹配以/开始最短的字符串,通常可以获取到文件路径前缀)
- c:/windows
- $ echo ${test%%/*} (删除 从尾部匹配以/开始最长的字符串)
- c:
字符串替换
- 表达式规则
- ${string/substring/replacement} 使用$replacement, 来代替第一个匹配的$substring
- ${string//substring/replacement} 使用$replacement, 代替所有匹配的$substring
- ${string/#substring/replacement} 如果$string的前缀匹配$substring, 那么就用$replacement来代替匹配到的$substring
- ${string/%substring/replacement} 如果$string的后缀匹配$substring, 那么就用$replacement来代替匹配到的$substring
- 测试列子:
- str="apple, tree, apple tree"
- echo ${str/apple/APPLE} # 替换第一次出现的apple
- APPLE, tree, apple tree
- echo ${str//apple/APPLE} # 替换所有apple
- APPLE, tree, APPLE tree
- echo ${str/#apple/APPLE} # 如果字符串str以apple开头,则用APPLE替换它
- APPLE, tree, apple tree
- echo ${str/%apple/APPLE} # 如果字符串str以apple结尾,则用APPLE替换它(str是以tree结尾的)
- apple, tree, apple tree
- 测试列子2:
- $ test='c:/windows/boot.ini'
- $ echo ${test/\//\\} (匹配的为\/,匹配的子串/需要转义,所以添加\,同理替换的字符串\转义为\\)
- c:\windows/boot.ini
- $ echo ${test//\//\\}
- c:\windows\boot.ini
- #${变量/查找/替换值} 一个“/”表示替换第一个,”//”表示替换所有,当查找中出现了:”/”请加转义符”\/”表示。
字符串比较 四种判断方式 == != < >
(按 ascii 值比较大小), ==
相等则为真,!=
不相等则为真。
- str="bacd"
- ##判断字符串是否相等 这里的== 也可以换做 = 但是一般情况下使用==
- if [ "$str"x == "abcdsd"x ]; then
- echo "相等"
- fi
- ##不相等则为真
- if [ "$str"x != "abcdsd"x ]; then
- echo "不相等"
- fi
- ##注意 比较字符串是否相等,可以字符串后面+上个别的字符,如果str为空的话,可以防止表达式报错:[: =: unary operator expected
实践要求
按照右侧“代码文件”中提示的要求补全代码,本关的目的,是希望通过学习,对于字符串的使用能有一个大致的了解,在实际工作中遇到需要对某一个字符串处理的时候,能够找到对应的方法。
任务描述
本关将带领大家熟悉 bash 的内置命令,并掌握一些常用的内置命令。 在开启此小节之前,相信你已经对 shell 编程的变量和字符串的使用有了一定的了解。需注意: 本课程没有描述 shell 数组的概念,因为在实际的操作中 shell 数组并不是很友好,一般在数据处理的模型都用 python 来代替 shell,如果大家对 shell 数组感兴趣,请同学们可以自行翻阅资料了解一下 shell 的数组概念。
相关知识
什么是内建命令
所谓 Shell 内建命令,就是由 Bash 自身提供的命令,而不是文件系统中的某个可执行文件。 例如,用于进入或者切换目录的 cd 命令,虽然我们一直在使用它,但如果不加以注意,很难意识到它与普通命令的性质是不一样的:该命令并不是某个外部文件,只要在 Shell 中你就可以运行这个命令。 可以使用 type
来确定一个命令是否是内建命令:
- root@Test-old-web01:/opt/f89vxyw7/task1# type cd
- cd is a shell builtin
- root@Test-old-web01:/opt/f89vxyw7/task1# type ifconfig
- ifconfig is /sbin/ifconfig
由此可见,cd 是一个 Shell 内建命令,而 ifconfig 是一个外部文件,它的位置是/sbin/ifconfig
。 还记得系统变量 $PATH
吗?$PATH
变量包含的目录中几乎聚集了系统中绝大多数的可执行命令,它们都是外部命令。
通常来说,内建命令会比外部命令执行得更快,执行外部命令时不但会触发磁盘 I/O,还需要 fork 出一个单独的进程来执行,执行完成后再退出。而执行内建命令相当于调用当前 Shell 进程的一个函数。
shell有哪些内建命令
下表列出了 Bash Shell 中直接可用的内建命令。
命令 说明
- : 扩展参数列表,执行重定向操作
- . 读取并执行指定文件中的命令(在当前 shell 环境中)
- alias 为指定命令定义一个别名
- bg 将作业以后台模式运行
- bind 将键盘序列绑定到一个 readline 函数或宏
- break 退出 for、while、select 或 until 循环
- builtin 执行指定的 shell 内建命令
- caller 返回活动子函数调用的上下文
- cd 将当前目录切换为指定的目录
- command 执行指定的命令,无需进行通常的 shell 查找
- compgen 为指定单词生成可能的补全匹配
- complete 显示指定的单词是如何补全的
- compopt 修改指定单词的补全选项
- continue 继续执行 for、while、select 或 until 循环的下一次迭代
- declare 声明一个变量或变量类型。
- dirs 显示当前存储目录的列表
- disown 从进程作业表中刪除指定的作业
- echo 将指定字符串输出到 STDOUT
- enable 启用或禁用指定的内建shell命令
- eval 将指定的参数拼接成一个命令,然后执行该命令
- exec 用指定命令替换 shell 进程
- exit 强制 shell 以指定的退出状态码退出
- export 设置子 shell 进程可用的变量
- fc 从历史记录中选择命令列表
- fg 将作业以前台模式运行
- getopts 分析指定的位置参数
- hash 查找并记住指定命令的全路径名
- help 显示帮助文件
- history 显示命令历史记录
- jobs 列出活动作业
- kill 向指定的进程 ID(PID) 发送一个系统信号
- let 计算一个数学表达式中的每个参数
- local 在函数中创建一个作用域受限的变量
- logout 退出登录 shell
- mapfile 从 STDIN 读取数据行,并将其加入索引数组
- popd 从目录栈中删除记录
- printf 使用格式化字符串显示文本
- pushd 向目录栈添加一个目录
- pwd 显示当前工作目录的路径名
- read 从 STDIN 读取一行数据并将其赋给一个变量
- readarray 从 STDIN 读取数据行并将其放入索引数组
- readonly 从 STDIN 读取一行数据并将其赋给一个不可修改的变量
- return 强制函数以某个值退出,这个值可以被调用脚本提取
- set 设置并显示环境变量的值和 shell 属性
- shift 将位置参数依次向下降一个位置
- shopt 打开/关闭控制 shell 可选行为的变量值
- source 读取并执行指定文件中的命令(在当前 shell 环境中)
- suspend 暂停 Shell 的执行,直到收到一个 SIGCONT 信号
- test 基于指定条件返回退出状态码 0 或 1
- times 显示累计的用户和系统时间
- trap 如果收到了指定的系统信号,执行指定的命令
- type 显示指定的单词如果作为命令将会如何被解释
- typeset 声明一个变量或变量类型。
- ulimit 为系统用户设置指定的资源的上限
- umask 为新建的文件和目录设置默认权限
- unalias 刪除指定的别名
- unset 刪除指定的环境变量或 shell 属性
- wait 等待指定的进程完成,并返回退出状态码
常用的内建命令
一下子掌握所有的内建命令难度要求比较大,我们可以从常用内建命令入手,第一个内置命令: alias
。 1、alias alias 用来给命令创建一个别名。若直接输入该命令且不带任何参数,则列出当前 Shell 进程中使用了哪些别名,如下:
- root@Test-old-web01:/opt/f89vxyw7/task1# alias
- alias egrep='egrep --color=auto'
- alias fgrep='fgrep --color=auto'
- alias grep='grep --color=auto'
- alias l='ls -CF'
- alias la='ls -A'a
- alias ll='ls -alF'
- alias ls='ls --color=auto'
现在你应该能理解类似ll
这样的命令为什么与ls -l
或者 ls -alF
的效果是一样的吧,是因为ls -alF
被重新命名为ll
。因此,在命令行输入 ll
,即相当于输入了 ls -alF
。
使用 alias 命令自定义别名的语法格式为(临时):
- alias new_name='command'
通过 date 命令可以获得当前的 UNIX 时间戳,具体写法为 date +%s
,如果你嫌弃它太长或者不容易记住,那可以给它定义一个别名。
- alias timestamp='date +%s'
这样你在命令行输入 timestamp
获得的效果及跟输入date +%s
一样,如下:
- root@Test-old-web01:/opt/f89vxyw7/task1# date +%s
- 1580972143
- root@Test-old-web01:/opt/f89vxyw7/task1# alias timestamp='date +%s'
- root@Test-old-web01:/opt/f89vxyw7/task1# timestamp
- 1580972158
注意:通过以上方式命令别名后,在退出当前窗口后,刚设定的 alias 命令别名也就消失了,如何才能永久使用了?请看下面的介绍:
首先,如果想让别名能够在不同的 shell 会话中使用,就需要把它们添加到 shell 用来保存别名的文件中。大多数情况下,这个文件不是 .bashrc
,就是 .bash_aliases
,我们使用的系统 CentOS,是保存在.bashrc
里。(或者可以在 root 家目录下输入:ls -a ~
看到那个文件)
如果要永久保存,就把alias new_name='command'
给编辑到 /root/.bashrc
下,然后:wq
保存退出,但得重新加载别名文件,才能生效,可执行 source ~/.bashrc
(这样更改后只有 root 用户的别名生效,要想普通用户有效,普通用户目录下的.bashrc
也必需作同样的修改!比如现在有用户为 jerry,则需要到其/home/jerry/.bashrc
下编辑然后执行source ~/.bashrc
)。
- ##pdl用户登录,在/home/pdl加目录下 或者 "~"及代表用户的家目录
- ## /home/pdl/.bashrc 某行添加
- ## alias homeview='ll ~'
- ##添加
- pdl@Test-old-web01:~$ vim /home/pdl/.bashrc
- ##重新加载别名文件
- pdl@Test-old-web01:~$ source ~/.bashrc
- ##执行homeview 即相当于 执行 ll ~
- pdl@Test-old-web01:~$ homeview
- total 36
- drwx------ 4 pdl pdl 4096 Feb 6 15:10 ./
- drwxr-xr-x 5 root root 4096 Dec 30 11:18 ../
- -rw------- 1 pdl pdl 1114 Nov 21 16:54 .bash_history
- -rw-r--r-- 1 pdl pdl 220 Sep 1 2015 .bash_logout
- -rw-r--r-- 1 pdl pdl 3795 Feb 6 15:10 .bashrc
- drwx------ 2 pdl pdl 4096 Aug 22 09:00 .cache/
- -rw-r--r-- 1 pdl pdl 655 May 16 2017 .profile
- drwx------ 2 pdl pdl 4096 Aug 22 15:11 .ssh/
- -rw------- 1 pdl pdl 1016 Feb 6 15:10 .viminfo
- ##退出终端重新加载,发现别名homeview永久存在
实际用例:假设生产环境有三台机器127.1.1.2
;127.1.1.3
;127.1.1.4
; 你可以通过 alias 的方式简化 ssh 登录方式,操作如下: 在~/.bashrc
下末尾行追加内容。然后 source ~/.bashrc
,这样即可以在命令行直接输入 ssh_01
,ssh_02,ssh_03
快速登陆。
- alias ssh_01= 'sshpass -p 密码 ssh -o StrictHostKeyChecking=no -p 22 root@127.1.2'
- alias ssh_02= 'sshpass -p pass2 ssh -o StrictHostKeyChecking=no -p 22 root@127.1.1.3'
- alias ssh_03= 'sshpass -p pass4 ssh -o StrictHostKeyChecking=no -p 22 root@127.1.1.4'
实践要求
本关没有评测,希望大家独立在命令行按照示例自己操作一遍,了解熟悉 shell 的内置命令,并掌握 alias 的使用方式以及应用场景。
任务描述
本关继续为大家介绍 shell 的内置命令 echo 和 read,通过学习掌握 echo 和 read 的用法。
相关知识
内置命令——echo
echo 是一个 Shell 内建命令,用来在终端输出字符串,并在最后默认加上换行符。请看下面的例子:
- #!/bin/bash
- name="在线编程"
- url="www.educoder.net"
- echo "小哥哥,小姐姐,你好!" #直接输出字符串
- echo $url #输出变量
- echo "${name}的网址是:${url}" #双引号包围的字符串中可以解析变量
- echo '${name}的网址是:${url}' #单引号包围的字符串中不能解析变量
输出结果为:
- [root@pre-host-work02 opt]# bash a.sh
- 小哥哥,小姐姐,你好!
- www.educoder.net
- 在线编程的网址是:www.educoder.net
echo 命令输出结束后默认会换行,如果不希望换行,可以加上-n
参数,如下所示:
- #!/bin/bash
- name="在线编程"
- url="www.educoder.net"
- echo -n "小哥哥,小姐姐,你好!" #直接输出字符串
- echo -n $url #输出变量
- echo "${name}的网址是:${url}" #双引号包围的字符串中可以解析变量
输出结果为:
- [root@pre-host-work02 opt]# bash a.sh
- 小哥哥,小姐姐,你好!www.educoder.net在线编程的网址是:www.educoder.net
默认情况下,echo 不会解析以反斜杠\
开头的转义字符。比如,\n
表示换行,echo 默认会将它作为普通字符对待。请看下面的例子:
- [root@localhost ~]# echo "hello \nworld"
- hello \nworld
我们可以添加-e
参数来让 echo 命令解析转义字符。例如:
- [root@localhost ~]# echo -e "hello \nworld"
- hello
- world
内置变量——read
read 是 Shell 内置命令,用来从标准输入中读取数据并赋值给变量。如果没有进行重定向,默认就是从键盘读取用户输入的数据;如果进行了重定向,那么可以从文件中读取数据(后续我们会在 shell 的输入和输出中深入讲解重定向的概念,不了解的同学可以不用理会,暂时就认为:read 命令就是从键盘读取数据)。
read 命令的用法为:
- read [-options] [variables]
options 表示选项,如下表所示;variables 表示用来存储数据的变量,可以有一个,也可以有多个。
options 和 variables 都是可选的,如果没有提供变量名,那么读取的数据将存放到环境变量 REPLY 中。 Shell read 命令支持的选项:
【实例1】使用 read 命令给多个变量赋值,并打印提示语 read -p
。
- #!/bin/bash
- read -p "Enter some information > " name age
- echo "网站名字:$name"
- echo "年龄:$age"
输出结果为:
- [root@pre-host-work02 opt]# bash a.sh
- Enter some information > educoder 20
- 网站名字:educoder
- 年龄:20
Tips: 如果用 read 交互参数,输入错了,请按住Ctrl+Backspace清除而不是只按住Backspace。
【示例2】只读取一个字符。
- #!/bin/bash
- read -n 1 -p "Enter a char > " char
- printf "\n" #换行
- echo $char
运行结果:
- Enter a char > 1
- 1
【实例3】在指定时间内输入密码。
- #!/bin/bash
- if
- read -t 20 -sp "Enter password in 20 seconds(once) > " pass1 && printf "\n" && #第一次输入密码
- read -t 20 -sp "Enter password in 20 seconds(again)> " pass2 && printf "\n" && #第二次输入密码
- [ $pass1 == $pass2 ] #判断两次输入的密码是否相等
- then
- echo "Valid password"
- else
- echo "Invalid password"
- fi
这段代码中,我们使用**&&**组合了多个命令,相当于其他语言中的 and ,这些命令会依次执行,并且从整体上作为 if 语句的判断条件。只要其中一个命令执行失败(退出状态为非 0 值),整个判断条件就失败了,后续的命令也就没有必要执行了。
- 如果两次输入密码相同,运行结果为:
- Enter password in 20 seconds(once) >
- Enter password in 20 seconds(again)>
- Valid password
- 如果两次输入密码不同,运行结果为:
- Enter password in 20 seconds(once) >
- Enter password in 20 seconds(again)>
- Invalid password
- 如果第一次输入超时,运行结果为:
- Enter password in 20 seconds(once) > Invalid password
- 如果第二次输入超时,运行结果为:
- Enter password in 20 seconds(once) >
- Enter password in 20 seconds(again)> Invalid password
关于 echo 和 read 的使用方式还有很多,大家可以通过 help echo 和 help read 的方式去获取更多的帮助文档。
任务描述
本关继续带领大家了解掌握 shell 的内置命令,接下来将会学习内置命令exit
和declare
。
相关知识
内置命令 —— exit
exit 是一个 Shell 内置命令,用来退出当前 Shell 进程,并返回一个退出状态;使用**$?**可以接收这个退出状态,$?
如果不记得是什么,请查看前一章节的shell变量。exit
命令可以接受一个整数值作为参数,代表退出状态。如果不指定,默认状态值是 0。
一般情况下,退出状态为 0 表示成功,退出状态为非 0 表示执行失败(出错)了, exit
退出状态只能是一个介于 0~255 之间的整数,其中只有 0 表示成功,其它值都表示失败。
Shell 进程执行出错时,可以根据退出状态来判断具体出现了什么错误,比如打开一个文件时,我们可以指定 1 表示文件不存在,2 表示文件没有读取权限,3 表示文件类型不对。 【实列1】运行该脚本:
- #!/bin/bash
- echo "befor exit"
- exit 8
- echo "after exit
- [root@localhost ~]$ bash ./test.sh
- befor exit
可以看到,"after exit"并没有输出,这说明遇到 exit 命令后,test.sh 执行就结束了。 注意:exit 表示退出当前 Shell 进程,我们必须在新进程中运行 test.sh,否则当前 Shell 会话(终端窗口)会被关闭,我们就无法看到输出结果了。 我们可以紧接着使用$?
来获取 test.sh 的退出状态:
- [root@localhost ~]$ echo $?
- 8
内置命令 —— declare
declare
一般很少用到,但大家要知道下列常用的方法。
【实列2】declare 将变量声明为整数并进行计算。
- #!/bin/bash
- declare -i m n ret #将多个变量声明为整数
- m=10
- n=30
- ret=$m+$n
- echo $ret
如果我们不使用 declare,结果如下所示:
- [root@pre-host-work02 ~]# a=10
- [root@pre-host-work02 ~]# b=20
- [root@pre-host-work02 ~]# c=$a+$b
- [root@pre-host-work02 ~]# echo $c
- 10+20
因此注意在 shell 如果要求整数值的话,不要忘了有 declare 这样的内置命令来定义变量。关于 declare 的其他使用方式,大家可以自行去了解。
编程要求
补全右侧编辑器 Begin-End 区间的代码,完成指定功能,具体要求如下:
1.现有变量 a、b、c ,已知 a 为整数 100,b 为整数 300,c=$a+$b
,打印 c 的值,应该为 400; 2.补全[ $? -eq ]内的值,-eq 的意思是‘等于’,注意 [ ] 两边要留一个空格,通过 $? 判断上面的命令是否运行成功,如果运行成功则输出success
,否则输出 faild
。
注意:请不要直接输出 400 或者直接输出 success
,因为评测程序会判断你的代码文件。
任务描述
本关任务:掌握函数的创建和使用方法。
相关知识
函数是一个脚本代码块,你可以为其命名并在代码中任何位置重用。要在脚本中使用该代码块时,只要使用所起的函数名就行了(这个过程称为调用函数)。本关将会介绍如何在 shell 脚本中创建和使用函数。
创建函数
有两种格式可以用来在 bash shell 脚本中创建函数。
- 第一种格式采用关键字 function,后跟分配给该代码块的函数名。
- function name {
- commands
- }
参数说明:
- name 属性定义了赋予函数的唯一名称。脚本中定义的每个函数都必须有一个唯一的名称。
- commands 是构成函数的一条或多条 bash shell 命令。在调用该函数时,bash shell 会按命令在函数中出现的顺序依次执行,就像在普通脚本中一样。
- 定义函数的第二种格式更接近于其他编程语言中定义函数的方式。
- i. name() {
- ii. commands
- iii. }
函数名后的空括号表明正在定义的是一个函数。这种格式的命名规则和之前定义 shell 脚本函数的格式一样。
使用函数
【实例1】要在脚本中使用函数,只需要像其他 shell 命令一样,在行中指定函数名就行了。 已知 cat test.sh
显示内容如下:
- #!/bin/bash
- # using a function in a script
- function func1 {
- echo "This is an example of a function"
- }
- count=1
- while [ $count -le 5 ]
- do
- func1
- count=$[ $count + 1 ]
- done
- echo "This is the end of the loop"
- func1 # 指定函数名即可调用函数
- echo "Now this is the end of the script"
执行输出结果如下:
- This is an example of a function
- This is an example of a function
- This is an example of a function
- This is an example of a function
- This is an example of a function
- This is the end of the loop
- This is an example of a function
- Now this is the end of the script
每次引用函数名 func1 时,bash shell 会找到 func1 函数的定义并执行你在那里定义的命令。 【实例2】函数的调用要在函数定义之后使用,否则会报错。
- #!/bin/bash
- echo "hello function"
- fun1
- fun1(){
- echo "i am func1"
- }
执行结果如下:
- # bash a.sh
- hello function
- a.sh: line 6: fun1: command not found
【实例3】函数名必须是唯一的,否则也会有问题。如果你重定义了函数,新定义会覆盖原来函数的定义,这一切不会产生任何错误消息。
- #!/bin/bash
- fun1(){
- echo "i am func1"
- }
- fun1(){
- echo "i am new func1"
- }
- fun1
执行结果如下:
- i am new func1
编程要求
在右侧编辑器 Begin-End 区间补充代码,调用 welcome 函数输出指定内容。
任务描述
本关任务:学习掌握什么是 shell 函数的返回值。
相关知识
bash shell 会把函数当作一个小型脚本,运行结束时,会返回一个退出状态码。有 3 种不同的方法来为函数生成退出状态码。
默认退出状态码
默认情况下,函数的退出状态码是函数中最后一条命令返回的退出状态码。在函数执行结束后,可以用标准变量$?
来确定函数的退出状态码。 【实例1】已知 test1.sh 脚本内容如下:
- #!/bin/bash
- func1() {
- echo "trying to display a non-existent file"
- ls -l badfile
- }
- echo "testing the function: "
- func1
- echo "The exit status is: $?"
运行脚本 bash test1.sh
执行结果如下:
- testing the function:
- trying to display a non-existent file
- ls: cannot access badfile: No such file or directory
- The exit status is: 2
函数的退出状态码是 2,这是因为函数中的最后一条命令没有成功运行。但你无法知道函数中其他命令中是否成功运行。带着这个疑问再看下面的例子:
【实例2】已知 test2.sh 脚本内容如下:
- #!/bin/bash
- func1() {
- ls -l badfile
- echo "trying to display a non-existent file"
- }
- echo "testing the function: "
- func1
- echo "The exit status is: $?"
运行脚本 bash test1.sh
执行结果如下:
- testing the function:
- ls: cannot access badfile: No such file or directory
- trying to display a non-existent file
- The exit status is: 0
这次,由于函数最后一条语句 echo 运行成功,该函数的退出状态码就是 0,尽管其中有一条命令并没有正常运行。由此可见,使用函数的默认退出状态码,来判断函数是否正常运行是很危险的。幸运的是,有几种办法可以解决这个问题。
return 命令
bash shell 使用 return 命令退出函数,并返回特定的退出状态码。return 命令允许指定一个整数值来定义函数的退出状态码,从而提供了一种简单的途径来设定函数退出状态码。 【实例3】已知 test3.sh 脚本的内容如下:
- #!/bin/bash
- func1() {
- ls -l badfile
- echo "trying to display a non-existent file"
- return 20
- }
- echo "testing the function: "
- func1
- echo "The exit status is: $?"
执行bash test.sh
显示如下:
- testing the function:
- ls: cannot access badfile: No such file or directory
- trying to display a non-existent file
- The exit status is: 20
这一次变量$?
返回的结果是 20,而按照默认的返回 $?
应该是 0,因为函数的最后一条命令正确执行。
任务描述
本关任务:掌握函数传递参数的方法以及如何处理变量。
相关知识
向函数传递参数
我们在 shell 变量那个实训,介绍过了一些特殊变量 $0
、$1
、$#
等。在 shell 中,函数可以使用标准的参数环境变量来表示命令行上传给函数的参数。例如,函数名会在$0
变量中定义,函数命令行上的任何参数都会通过$1
、$2
等定义。也可以用特殊变量$#
,来判断传给函数的参数数目。请参考实例 1:
【实例1】已知脚本 test.sh 的内容如下:
- function addem {
- if [ $# -eq 0 ] || [ $# -gt 2 ] # 判断传递给函数的参数个数
- then
- echo -1
- elif [ $# -eq 1 ]
- then
- echo $[ $1 + $1 ]
- else
- echo $[ $1 + $2 ]
- fi
- }
- echo -n "Adding 10 and 15: "
- value=$(addem 10 15)
- echo $value
- echo -n "Let's try adding just one number: "
- value=$(addem 10)
- echo $value
- echo -n "Now trying adding no numbers: "
- value=$(addem)
- echo $value
- echo -n "Finally, try adding three numbers: "
- value=$(addem 10 15 20)
- echo $value
输出结果如下:
- Adding 10 and 15: 25
- Let's try adding just one number: 20
- Now trying adding no numbers: -1
- Finally, try adding three numbers: -1
这段脚本的意思是:脚本中的 addem 函数首先会检查脚本传给它的参数数目。如果没有任何参数,或者参数多于两个,addem 会返回值 -1;如果只有一个参数,addem 会将参数与自身相加;如果有两个参数,addem 会将它们进行相加。
注意:由于函数使用特殊参数环境变量作为自己的参数值,因此它无法直接获取脚本在命令行中的参数值。下面的【实例2】将会运行失败。
【实例2】:已知 test2.sh 的内容如下:
- function badfunc1 {
- echo $[ $1 * $2 ]
- }
- badfunc1
我在命令行执行 bash test2.sh 10 3
,发现脚本报错:
- [root@pre-host-work02 opt]# bash a.sh 1 10
- a.sh: line 3: * : syntax error: operand expected (error token is "* ")
尽管函数 badfunc1 也使用了 $1 和 $2 变量,但它们和脚本主体中的 $1 和 $2 变量并不相同。要在函数中使用这些值,必须在调用函数时手动将它们传过去。我们把脚本改动如下:
- function badfunc1 {
- echo $[ $1 * $2 ]
- }
- badfunc1 $1 $2
通过将$1
和$2
变量传给函数,它们就能跟其他变量一样供函数使用了,我在命令行执行 bash test2.sh 10 3
,结果符合预期:
- [root@pre-host-work02 opt]# bash a.sh 10 3
- 30
函数中处理变量
函数使用两种类型的变量:
- 全局变量
- 局部变量
全局变量
全局变量是在 shell 脚本中任何地方都有效的变量。如果你在脚本的主体部分定义了一个全局变量,那么可以在函数内读取它的值。类似地,如果你在函数内定义了一个全局变量,可以在脚本的主体部分读取它的值。 默认情况下,你在脚本中定义的任何变量都是全局变量。在函数外定义的变量可在函数内正常访问,请看【实例3】。
【实列3】查看 test3.sh 内容,执行cat test3.sh
得到如下内容:
- #!/bin/bash
- function dbl {
- value=$[ $value * 2 ]
- }
- value=450
- dbl
- echo "The new value is: $value"
执行脚本输出结果如下:
- The new value is: 900
$value
变量在函数外定义并被赋值。当 dbl 函数被调用时,该变量及其值在函数中都依然有效。如果变量在函数内被赋予了新值,那么在脚本中引用该变量时,新值也依然有效。但这其实很危险,尤其是如果你想在不同的 shell 脚本中使用函数的话。它要求你清清楚楚地知道函数中具体使用了哪些变量,包括那些用来计算非返回值的变量。请看【实例4】。
【实例4】:
- #!/bin/bash
- function func1 {
- temp=$[ $value + 5 ]
- result=$[ $temp * 2 ]
- }
- temp=4
- value=6
- func1 # 这个时候全局变量temp=6+5及为11,result=22
- echo "The result is $result"
- if [ $temp -gt $value ]
- then
- echo "temp is larger"
- else
- echo "temp is smaller"
- fi
执行脚本输出结果如下:
- The result is 22
- temp is larger
局部变量
无需在函数中使用全局变量,函数内部使用的任何变量都可以被声明成局部变量。要实现这一点,只要在变量声明的前面加上 local 关键字就可以了,比如local temp
。或者也可以在变量赋值语句中使用 local 关键字:local temp=$[ $value + 5 ]
。 local 关键字保证了变量只局限在该函数中。如果脚本中在该函数之外有同样名字的变量,那么 shell 将会保持这两个变量的值是分离的。现在你就能很轻松地将函数变量和脚本变量隔离开了,只共享需要共享的变量。
【实例5】:
- #!/bin/bash
- # demonstrating the local keyword
- function func1 {
- local temp=$[ $value + 5 ]
- result=$[ $temp * 2 ]
- }
- temp=4
- value=6
- func1 #这个时候全局部变量temp=6+5,但是全局变量temp=4.
- echo "The result is $result"
- if [ $temp -gt $value ]
- then
- echo "temp is larger"
- else
- echo "temp is smaller"
- fi
执行脚本输出结果如下:
- The result is 22
- temp is smaller
现在,在 func1 函数中使用 $temp
变量时,并不会影响在脚本主体中赋给$temp
变量的值。请重点分析【实例4】和【实例5】中的不同。
编程要求
本关不进行考核,希望大家能够真正的理解:
- 向函数传递变量的方法;
- 在函数中使用局部变量和全局变量的区别。 这两点非常重要,希望大家能够在命令行中自行练习本关【实例】中代码,能够充分的理解掌握这两点内容。
任务描述
本关任务:掌握文件包含方法的使用。
相关知识
文件包含方法
在 C,C++,PHP 中都是用 include 来包含文件,Go 和 Java 使用 import 来包含(导入)包,而在 shell 中,很简单,只需要一个点.
,然后跟着文件路径及文件名,或者使用source
关键字也可以,注意文件路径可以使用绝对路径和相对路径。请看实例 1。
【实例1】已知在同一目录下,有 test1.sh 和 test2.sh 两个脚本,test1.sh 内容如下:
- #!/bin/bash
- url="www.educoder.net"
同一目录下的 test2.sh 内容如下:
- #!/bin/bash
- . ./test1.sh # 第一个.是包含符号,第二个. 表示当前目录下。
- # 这是采用相对路径包含的学法,记得中间要空格。
- echo "编程平台地址是:$url"
在命令行执行bash test2.sh
,大家想想看这个时候输出的是什么? 输出的结果如下:
- 编程平台地址是:www.educoder.net
注意:
- 变量的定义也要考虑局部变量和全局变量;
- 如实例1 所示 test1.sh 并不需要赋予执行权限;
- 注意包含文件的路径。
【实例2】同一目录下 test1.sh、test2.sh、test3.sh 内容如下:
- [root@pre-host-work02 opt]# cat test1.sh
- #!/bin/bash
- test(){
- url='www.educoder.net'
- }
- [root@pre-host-work02 opt]# cat test2.sh
- #!/bin/bash
- hello(){
- echo "i am from test3.sh"
- }
- [root@pre-host-work02 opt]# cat test3.sh
- #!/bin/bash
- ##包含多个文件时,一行智能写一个文件,分开写。
- . ./test1.sh ##包含test1.sh ./test1.sh表示当前目录下
- . ./test2.sh ##包含test2.sh ./test2.sh表示当前目录下
- echo "编程平台:$url"
- hello
在命令行执行bash test3.sh
,输出结果如下:
- [root@pre-host-work02 opt]# bash test3.sh
- 编程平台:
- i am from test3.sh
- 因为在 test1.sh 中 url 变量定义在函数内,即为局部变量,所以包含过来之后不能被调用;
- test2.sh 中定义了函数 hello,在 test3.sh 通过
.
的方式包含进来,通过函数名 hello 的调用,调用了 test2.sh 中的 hello()函数,输出“i am from test3.sh” 。
编程要求
本关任务包含了三个脚本文件,它们分别是 step4/t1.sh
、step4/t2.sh
、 step4/main.sh
,点击右侧“代码文件”即可查看,如下图所示:
- 根据要求补全
step4/t1.sh
代码。 - 根据要求补全
step4/main.sh
代码。