本文总结了Linux中关于Shell的一些基础知识
Linux中的Shell
在Linux的Shell编程中遵循一切皆命令
前言
shell中的bash本质上是一个解释器(启动器), 对于解释器来说, 输入可以是两种形式:
对于脚本文件而言(文本文件输入), 其本质是在文件首行声明执行所需要的解释器, 如:
#!/bin/bash
#!/usr/bin/python
命令读取方式
在执行时bash会首先启动对应声明的解释器, 然后逐行处理脚本文件;
例1: 在当前bash执行文件
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
| [root@490de829cb74 ~]# cat file.txt echo "Hello world" echo $$ echo $test
[root@490de829cb74 ~]# echo $$ 15
[root@490de829cb74 ~]# test=100 [root@490de829cb74 ~]# echo $test 100
[root@490de829cb74 ~]# source file.txt Hello world 15 100
[root@490de829cb74 ~]# . file.txt Hello world 15 100
|
注:
使用source或者.来逐条执行文件中的命令时, 会在当前bash中执行, 所以他们的$$(进程ID是相同的!)
例2: 在新的bash中读取文件执行
1 2 3 4 5 6 7 8 9 10 11 12 13
| [root@490de829cb74 ~]# cat file.txt echo "Hello world" echo $$ echo $test
[root@490de829cb74 ~]# echo $$ 15
[root@490de829cb74 ~]# bash file.txt Hello world 45
|
可见使用bash命令会创建一个新的bash, 并在新的bash中执行命令!
同时:
在新的bash中并未定义test变量, 所以echo $test为空
例3: 将文件声明为脚本, 并附上可执行权限
1 2 3 4 5 6 7 8 9 10 11
| [root@490de829cb74 ~]# cat file.txt
echo "Hello world" echo $$ echo $test
[root@490de829cb74 ~]# ./file.txt Hello world 51
|
和使用bash命令执行类似, 在执行可执行文件时, 也会创建一个新的bash, 并在新的bash中逐行执行命令
函数
在shell中也可以定义函数, 如下:
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
| [root@490de829cb74 ~]# var1=22
[root@490de829cb74 ~]# test() { > echo $$ > echo "hello world" > echo $var1 > }
[root@490de829cb74 ~]# test 15 hello world 22
[root@490de829cb74 ~]# type test test is a function test () { echo $$; echo "hello world"; echo $var1 }
|
注:
在linux中一切皆命令; 在使用时直接使用函数名执行即可!
IO重定向
首先, 重定向不是命令;
在Linux中每个程序启动时都会拿到自己对应的文件描述符(/proc/$pid/fd目录下), 如下:
1 2 3 4 5 6
| [root@490de829cb74 fd]# ll total 0 lrwx------ 1 root root 64 Apr 7 01:07 0 -> /dev/pts/1 lrwx------ 1 root root 64 Apr 7 01:07 1 -> /dev/pts/1 lrwx------ 1 root root 64 Apr 7 01:07 2 -> /dev/pts/1 lrwx------ 1 root root 64 Apr 7 01:48 255 -> /dev/pts/1
|
默认情况下, 每个程序都会有0, 1, 2描述符分别表示标准输入流, 标准输出流, 错误输出流;
而程序每多一个IO就会多一个文件描述符;
输出流
所以可以控制文件描述符的输出到不同的地方(IO重定向)
例1: 控制ls -l /命令的标准输出到ls.txt文件中
1 2 3 4 5 6 7 8 9 10 11
| [root@490de829cb74 ~]# ls -l / 1> ls.txt [root@490de829cb74 ~]# cat ls.txt total 92 dr-xr-xr-x 1 root root 4096 Apr 5 01:05 bin drwxr-xr-x 3 root root 4096 Apr 5 01:05 boot drwxr-xr-x 5 root root 360 Apr 7 01:04 dev drwxr-xr-x 1 root root 4096 Apr 6 06:09 etc drwxr-xr-x 2 root root 4096 Sep 23 2011 home dr-xr-xr-x 1 root root 4096 Apr 6 05:11 lib dr-xr-xr-x 1 root root 4096 Apr 5 01:05 lib64 ...
|
需要注意的是:
1代表命令对应的文件描述符;
>代表覆盖写入;
>>代表追加写入;
- 重定向操作符和文件描述符之间不可存在空白符!
例2: 控制ls -l /god/命令的错误输出到ls.txt文件中
1 2 3
| [root@490de829cb74 ~]# ls -l /god 2> ls_error.txt [root@490de829cb74 ~]# cat ls_error.txt ls: cannot access /god: No such file or directory
|
例3: 同时对标准和错误输出重定向
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| [root@490de829cb74 ~]# ls -l / /god 1>ls.txt 2>ls_error.txt [root@490de829cb74 ~]# cat ls.txt /: total 92 dr-xr-xr-x 1 root root 4096 Apr 5 01:05 bin drwxr-xr-x 3 root root 4096 Apr 5 01:05 boot drwxr-xr-x 5 root root 360 Apr 7 01:04 dev drwxr-xr-x 1 root root 4096 Apr 6 06:09 etc drwxr-xr-x 2 root root 4096 Sep 23 2011 home ......
[root@490de829cb74 ~]# cat ls_error.txt ls: cannot access /god: No such file or directory
|
例4: 文件描述符之间相互指向
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
|
[root@490de829cb74 ~]# ls -l / /god 2>& 1 1> ls_mix.txt ls: cannot access /god: No such file or directory
[root@490de829cb74 ~]# ls -l / /god 1> ls_mix.txt 2>& 1 [root@490de829cb74 ~]# cat ls_mix.txt ls: cannot access /god: No such file or directory /: total 92 dr-xr-xr-x 1 root root 4096 Apr 5 01:05 bin drwxr-xr-x 3 root root 4096 Apr 5 01:05 boot drwxr-xr-x 5 root root 360 Apr 7 01:04 dev drwxr-xr-x 1 root root 4096 Apr 6 06:09 etc ......
|
在文件描述符之间相互切换流指向时, 需要使用x>& y
因为如果只使用了>则会认为后面一个接的是文件名称!
其次, 流的转换是有先后顺序的(从左到右)
对于方法一:
首先将错误输出流2指向了1(此时标准输入流1仍然指向的是屏幕!), 然后修改了标准输入流1指向文件ls_mix.txt.
所以最终ls_mix.txt中存放的是标准输入流1的输出, 而错误流2被输出到了屏幕;
对于方法二:
首先改变了标准输入流1指向文件, 然后再修改错误输出流2. 使得两个流的输出都指向了文件;
例5: 合并标准输出和错误输出流
1 2 3 4 5 6 7 8 9
| [root@490de829cb74 ~]# ls -l / /god &> ls_mix.txt [root@490de829cb74 ~]# ls -l / /god >& ls_mix.txt [root@490de829cb74 ~]# cat ls_mix.txt ls: cannot access /god: No such file or directory /: total 92 dr-xr-xr-x 1 root root 4096 Apr 5 01:05 bin drwxr-xr-x 3 root root 4096 Apr 5 01:05 boot ......
|
除了使用流转换以外, 可以使用&>或者>&合并标准输出和错误输出流;
注: 此时流后应当接的是一个文件!
read命令
使用: read + 变量名
作用: 使用read后会阻塞, 将输入的值复制给变量(换行结束)
例:
1 2 3 4
| [root@490de829cb74 ~]# read test 12d12d312 [root@490de829cb74 ~]# echo $test 12d12d312
|
输入流
① <<<
<<<可以将右侧的数据输入到一个输入流文件描述符
例:
1 2 3
| [root@490de829cb74 ~]# read test 0<<<"hello" [root@490de829cb74 ~]# echo $test hello
|
② <<XXX
<<右侧可以指定输入流边界标志位, 并且在开始和结束标志位之间的内容可以被转化为标准输入流
例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| [root@490de829cb74 ~]# read test 0<<OOXX > 12412345 > faqf13 > 123d12d > OOXX [root@490de829cb74 ~]# echo $test 12412345
[root@490de829cb74 ~]# cat test.txt cat <<OOXX hello nihao bye OOXX
[root@490de829cb74 ~]# source test.txt hello nihao bye
|
③ <
可以使用0<直接将一个文件输入, 如:
1 2 3 4 5 6
| [root@490de829cb74 ~]# cat 0< test.txt cat <<OOXX hello nihao bye OOXX
|
显然, 没有什么实质性作用;
例: 使用IO流向百度发送HTTP请求
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
| [root@490de829cb74 fd]# ll /proc/15/fd/ total 0 lrwx------ 1 root root 64 Apr 7 01:07 0 -> /dev/pts/1 lrwx------ 1 root root 64 Apr 7 01:07 1 -> /dev/pts/1 lrwx------ 1 root root 64 Apr 7 01:07 2 -> /dev/pts/1 lrwx------ 1 root root 64 Apr 7 01:48 255 -> /dev/pts/1
[root@490de829cb74 fd]# exec 8<> /dev/tcp/www.baidu.com/80 [root@490de829cb74 fd]# ll /proc/15/fd/ total 0 lrwx------ 1 root root 64 Apr 7 01:07 0 -> /dev/pts/1 lrwx------ 1 root root 64 Apr 7 01:07 1 -> /dev/pts/1 lrwx------ 1 root root 64 Apr 7 01:07 2 -> /dev/pts/1 lrwx------ 1 root root 64 Apr 7 01:48 255 -> /dev/pts/1 lrwx------ 1 root root 64 Apr 7 01:07 8 -> socket:[273464]
[root@490de829cb74 fd]# echo -e "GET / HTTP/1.1\n" 1>& 8
[root@490de829cb74 fd]# cat 0<& 8 HTTP/1.1 200 OK Accept-Ranges: bytes Cache-Control: no-cache Connection: keep-alive Content-Length: 14615 Content-Type: text/html Date: Tue, 07 Apr 2020 02:49:44 GMT
<!DOCTYPE html><!--STATUS OK--> <html> <head> <title>百度一下,你就知道</title> <link href="http://s1.bdstatic.com/r/www/cache/static/home/css/index.css" rel="stylesheet" type="text/css" /> ......
|
上面的例子中首先通过exec 8<> /dev/tcp/www.baidu.com/80创建了一个8号文件描述符, 且是一个双向流;
注:
在/dev/tcp/中创建的文件描述符会触发内核机制, 最终创建了一个socket通信
如:
1 2 3 4 5 6 7
| >[root@490de829cb74 fd]# ll /proc/15/fd/ >total 0 >lrwx------ 1 root root 64 Apr 7 01:07 0 -> /dev/pts/1 >lrwx------ 1 root root 64 Apr 7 01:07 1 -> /dev/pts/1 >lrwx------ 1 root root 64 Apr 7 01:07 2 -> /dev/pts/1 >lrwx------ 1 root root 64 Apr 7 01:48 255 -> /dev/pts/1 >lrwx------ 1 root root 64 Apr 7 01:07 8 -> socket:[273464]
|
然后使用echo -e "GET / HTTP/1.1\n" 1>& 8将字符串输出流重定向到八号文件描述符(向百度发送请求头);
最后通过cat 0<& 8接收八号文件描述符的输入流(百度返回的响应), 最终拿到响应;
变量参数
bash中提供的变量可以分为:
本地变量
在当前shell中拥有, 并且变量的生命周期伴随shell.
例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| [root@490de829cb74 /]# name=god [root@490de829cb74 /]# echo $name god
[root@490de829cb74 /]# test=100 [root@490de829cb74 /]# func() { > echo $test > test=222 > }
[root@490de829cb74 /]# func 100
[root@490de829cb74 /]# echo $test 222
|
局部变量
在函数内部使用local定义的变量
例:
1 2 3 4 5 6
| [root@490de829cb74 /]# func() { > local inner=100 > } [root@490de829cb74 /]# func [root@490de829cb74 /]# echo $inner
|
位置变量
位置变量如: $1, $2, ${11}
位置变量可以出现在脚本或者函数中
特殊变量
$#: 位置参数个数
$*: 参数列表, 双引号引用为一个字符串
$@: 参数列表, 双引号引用为单独的字符串
$$: 当前shell的PID(相当于命令接收者)
$?: 上一个命令退出状态
例1: 使用特殊变量取参数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| [root@490de829cb74 /]# cat shell.sh echo $# echo $* echo $@
echo $1 echo $2 echo $11
[root@490de829cb74 /]# . shell.sh 1 2 3 4 5 6 7 8 9 0 a b 12 1 2 3 4 5 6 7 8 9 0 a b 1 2 3 4 5 6 7 8 9 0 a b 1 2 11
|
注意: echo $11打印的不是a, 而是11
这是因为: 实际上在解析的时候, 解析的是${1}1.
所以可以使用${11}取值
标准取值应该使用${xx}取值
例:
1 2 3 4 5 6 7
| [root@490de829cb74 /]# test=test [root@490de829cb74 /]# echo $test test [root@490de829cb74 /]# echo $testgood
[root@490de829cb74 /]# echo ${test}good testgood
|
例2: $$与管道
管道:
在管道的两侧会开辟两个新的子bash分别去执行
如:
1 2 3 4 5 6 7
| [root@490de829cb74 /]# test=100 [root@490de829cb74 /]# echo $test 100 [root@490de829cb74 /]# test=200 | echo ok ok [root@490de829cb74 /]# echo $test 100
|
上面经过管道后, 相当于在子bash中改变了test的值为200, 而不会影响到父bash中的test变量的值
在看下面的例子:
1 2 3 4 5 6 7 8 9
| [root@490de829cb74 /]# echo $$ 105 [root@490de829cb74 /]# echo $$ | cat 105
[root@490de829cb74 /]# echo $BASHPID 105 [root@490de829cb74 /]# echo $BASHPID | cat 134
|
对于$BASHPID可以看出管道的确创建了两个新的bash来执行;
但是为什么在子bash中, echo $$和父进程相同呢?
这是由$$的优先级决定的:
在echo $$ | cat命令中, 由于$$的优先级高于管道, 所以$$会被首先替换为105, 然后再去执行管道命令
而$BASHPID的优先级低于管道
环境变量
相关命令:
- set: 用来显示本地变量(包括当前用户的变量)
- unset: 取消变量
- env/printenv: 用来显示环境变量(显示当前用户的变量)
- export: 用来显示和设置环境变量(显示当前导出成用户变量的shell变量)
更多
每个shell有自己特有的变量(set)显示的变量,这个和用户变量是不同的,当前用户变量和你用什么shell无关,不管你用什么shell都在
比如HOME,SHELL等这些变量,但shell自己的变量不同shell是不同的,比如BASH_ARGC, BASH等,这些变量只有set才会显示,是bash特有的
export不加参数的时候,显示哪些变量被导出成了用户变量,因为一个shell自己的变量可以通过export “导出”变成一个用户变量
例1: 查看shell变量
1 2 3 4 5 6 7 8 9 10 11 12 13
| [root@490de829cb74 /]# set BASH=/bin/bash BASHOPTS=checkwinsize:cmdhist:expand_aliases:extquote:force_fignore:hostcomplete:interactive_comments:progcomp:promptvars:sourcepath BASHPID=105 BASH_ALIASES=() BASH_ARGC=() name=god test=100 func () { local inner=100 } ......
|
可以使用export标示一个变量被导出;
被导出的变量在当前shell创建一个子shell时, 子shell可以访问这个被导出变量的值;
例2: 使用export导出一个变量
1 2 3 4 5 6 7 8 9 10 11 12
| [root@490de829cb74 ~]# test=100
[root@490de829cb74 ~]# bash shell.sh hello
[root@490de829cb74 ~]# export test [root@490de829cb74 ~]# bash shell.sh hello 100
|
注: 此操作是导出, 而非父子shell共享变量
父子shell中的环境变量
由于子shell中可以获取到父shell中export的变量; 所以就有以下的问题:
- 在子shell中改变变量的值父shell中会变化吗?
- 在父shell中改变变量的值子shell中会变化吗?
① 在子shell中修改值
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
| [root@490de829cb74 ~]# echo $test 100
[root@490de829cb74 ~]# cat shell.sh echo "---------------" echo $test test=22222222 echo "---------------" echo $test sleep 20 echo $test
[root@490de829cb74 ~]# bash shell.sh & [1] 45 [root@490de829cb74 ~]# --------------- 100 --------------- 22222222
[root@490de829cb74 ~]# echo $test 100
[root@490de829cb74 ~]# 22222222 [1]+ Done bash shell.sh
[root@490de829cb74 ~]# echo $test 100
|
可见在子shell中对变量做出的任何修改在父shell中都是不可见的
(不会改变父shell中的值)
② 在父shell中修改值
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| [root@490de829cb74 ~]# echo $test 100
[root@490de829cb74 ~]# cat shell.sh echo "---------------" echo $test test=22222222 echo "---------------" echo $test sleep 20 echo $test
[root@490de829cb74 ~]# bash shell.sh & [1] 53 --------------- 100 --------------- 22222222
[root@490de829cb74 ~]# test=333
[root@490de829cb74 ~]# echo $test 333
[root@490de829cb74 ~]# 22222222 [1]+ Done bash shell.sh
|
可见在父shell中对变量做出的任何修改在子shell中也都是不可见的
(不会改变子shell中的值)
总结:
在Linux中使用内核中的fork()方法创建一个进程;
在fork()函数创建子进程时, 不会将原父进程中的所有数据全部拷贝一遍(如果父进程中含有大量大对象效率相当慢), 而是创建一个引用指向父进程中的对象等数据;
此时: 父变量->100<-子变量
而在父shell改变变量值时, shell才会真正去创建这个变量, 并分配新的内存空间;
此时: 父变量->200 子变量->100
子shell变量值改变时同理;
这样就保证了fork操作的效率, 同时保证了父子进程不可看到互相的改变;
引用
双引号引用
弱引用, 允许变量扩展(替换)
如:
1 2 3 4 5
| [root@490de829cb74 ~]# test=100 [root@490de829cb74 ~]# echo $test 100 [root@490de829cb74 ~]# echo "$test" 100
|
单引号引用
强引用, 不允许变量替换, 不可嵌套
如:
1 2 3 4 5
| [root@490de829cb74 ~]# test=100 [root@490de829cb74 ~]# echo $test 100 [root@490de829cb74 ~]# echo '$test' $test
|
注1: 花括号扩展不能被引用
注2: 命令执行前会删除引用符合
在shell中'和"是关键字, 在输出时不会输出. 如果想要输出, 需要转义, 如:
1 2 3 4
| >[root@490de829cb74 ~]# echo "$test" >100 >[root@490de829cb74 ~]# echo "\"$test\"" >"100"
|
命令替换
在bash执行时, 首先会将一些变量等字符串进行替换, 如:
例1: 使用反引号做命令替换
1 2 3 4 5 6 7 8 9 10 11 12
| [root@490de829cb74 ~]# var1=echo $test bash: 100: command not found
[root@490de829cb74 ~]# echo $var1
[root@490de829cb74 ~]# var1=`echo $test` [root@490de829cb74 ~]# echo $var1 100
|
首先使用var1=echo $test命令时, 会认为var1=echo是一个赋值命令, 然后$test(100)是另一个命令;
所以在执行时首先将$test1替换为100, 然后将var1赋值为echo, 然后执行命令100, 未找到所以报错;
此时由于shell执行命令是原子的, 所以输出var1的值为空
使用反引号包括后面语句则先执行了echo $test命令, 然后将结果赋值给var1;
除了使用反引号, 还可以使用$(command)
例2: 使用${()做命令替换
1 2 3
| [root@490de829cb74 ~]# var2=$(echo $test) [root@490de829cb74 ~]# echo $var2 100
|
同时命令替换是可以嵌套的
例3: 使用嵌套的命令替换
1 2 3
| [root@490de829cb74 ~]# var3=`echo $(echo $test)` [root@490de829cb74 ~]# echo $var3 100
|
命令退出状态
在shell中可以通过$?取得上一条命令的退出状态;
退出状态是一个数字,一般情况下,大部分命令执行成功会返回 0,失败返回其他;
例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| [root@490de829cb74 ~]# ls -l /root total 16 -rw------- 1 root root 2433 Apr 6 2017 anaconda-ks.cfg -rw-r--r-- 1 root root 7242 Apr 6 2017 install.log -rw-r--r-- 1 root root 1680 Apr 6 2017 install.log.syslog [root@490de829cb74 ~]# echo $? 0
[root@490de829cb74 ~]# ls -l /god ls: cannot access /god: No such file or directory [root@490de829cb74 ~]# echo $? 2
|
表达式
算术表达式
① 使用let
let命令可以计算算术运算表达式(可以使用help let查看)
例:
1 2 3 4 5 6 7 8 9 10 11
| [root@490de829cb74 ~]# a=100 [root@490de829cb74 ~]# b=200
[root@490de829cb74 ~]# echo $a+$b 100+200
[root@490de829cb74 ~]# let c=$a+$b [root@490de829cb74 ~]# echo $c 300
|
② 使用$[算术表达式]计算
例:
1 2 3 4 5
| [root@490de829cb74 ~]# a=100 [root@490de829cb74 ~]# b=200 [root@490de829cb74 ~]# c=$[$a+$b] [root@490de829cb74 ~]# echo $c 300
|
③ 使用$((算术表达式))计算
例:
1 2 3 4 5
| [root@490de829cb74 ~]# a=100 [root@490de829cb74 ~]# b=200 [root@490de829cb74 ~]# c=$(($a+$b)) [root@490de829cb74 ~]# echo $c 300
|
注: 使用两个小括号时, 其中的变量可以不加$符号
例2:
1 2 3 4 5 6 7 8
| [root@490de829cb74 ~]# a=1 [root@490de829cb74 ~]# b=2 [root@490de829cb74 ~]# ((a++)) [root@490de829cb74 ~]# echo $a 2 [root@490de829cb74 ~]# c=$((a+b)) [root@490de829cb74 ~]# echo $c 4
|
④ 使用expr计算
使用expr命令也可以计算算术表达式, 需要注意的是:
表达式中各个操作数和运算符之间必须要用空格, 且必须使用命令引用
例:
1 2 3 4 5 6 7 8 9 10 11 12
| [root@490de829cb74 ~]# a=100 [root@490de829cb74 ~]# b=200
[root@490de829cb74 ~]# c=`expr $a+$b` [root@490de829cb74 ~]# echo $c 100+200
[root@490de829cb74 ~]# c=`expr $a + $b` [root@490de829cb74 ~]# echo $c 300
|
条件表达式
条件表达式的几种:
[ expression ]
test expression
[[ expression ]]
可以使用help test查看
例:
1 2 3 4 5 6 7 8 9 10
| [root@490de829cb74 ~]# test 3 -gt 8 [root@490de829cb74 ~]# echo $? 1 [root@490de829cb74 ~]# test 3 -gt 2 [root@490de829cb74 ~]# echo $? 0 [root@490de829cb74 ~]# test 3 -gt 2 && echo ok ok [root@490de829cb74 ~]# test 3 -gt 8 && echo ok
|
例2: 使用[ exp ]
1 2 3 4 5
| [root@490de829cb74 ~]# [ 3 -gt 2 ] && echo ok ok
[root@490de829cb74 ~]# [ 3 -gt 8 ] && echo ok
|
注: 在Linux中命令和参数之间必须添加空白符, 所以[后面必须增加空格!