Shell编程——Shell变量简介(变量定义与使用、作用域、特殊变量等)

以下内容源于C语言中文网的学习与整理,非原创,如有侵权请告知删除。

一、与Shell变量有关的操作

脚本语言在定义变量时通常不需要指明类型,直接赋值就可以,Shell 变量也遵循这个规则。在 Bash shell 中,每一个变量的值都是字符串,无论你给变量赋值时有没有使用引号,值都会以字符串的形式存储。这意味着,Bash shell 在默认情况下不会区分变量类型,即使你将整数和小数赋值给变量,它们也会被视为字符串。当然,如果有必要,你也可以使用 declare 关键字显式定义变量的类型,但在一般情况下没有这个需求,Shell 开发者在编写代码时自行注意值的类型即可。

1、定义变量

(1)Shell 支持三种定义变量的方式:直接将字符串赋给变量(要求字符串不包含任何空白符,比如空格、Tab缩进等),用单引号包围字符串再赋值给变量,用双引号包围字符串再赋值给变量。如下所示,variable 是变量名,value 是赋给变量的值。

variable=value
variable='value'
variable="value"

(2)单引号和使用双引号的区别见下面的第4点,即“单引号原样输出,双引号解释后输出”。

(3)赋值号的周围不能有空格。

(4)Shell 变量的命名规范和大部分编程语言都一样。

  • 变量名由数字、字母、下划线组成;
  • 必须以字母或者下划线开头;
  • 不能使用 Shell 里的关键字(通过 help 命令可以查看保留关键字)。

(5)变量定义举例:

url=http://c.biancheng.net
echo $url
name='C语言中文网'
echo $name
author="糊涂"
echo $author

2、使用变量

(1)使用一个定义过的变量,只要在变量名前面加美元符号$即可。

author="糊涂"
echo $author
echo ${author}

(2)变量名外面的花括号{}是可选的,只是为了帮助解释器识别变量的边界。比如下面这种情况,如果不给 skill 这个变量加上花括号,解释器就会把 $skillScript 当成一个变量(其值为空),代码执行结果就不是我们期望的样子。

skill="Java"
echo "I am good at ${skill}Script"

3、修改变量的值

(1)已经定义过的变量,可以被重新赋值。

url="http://c.biancheng.net"
echo ${url}
url="http://c.biancheng.net/cpp/shell/"
echo ${url}

(2)注意,第二次对变量赋值时不能在变量名前加$,只有在使用变量时才能加$

4、单引号和双引号的区别

定义变量时,变量的值可以由单引号' '包围,也可以由双引号" "包围,它们有什么区别呢?

#!/bin/bash

url="http://c.biancheng.net"
website1='C语言中文网:${url}'
website2="C语言中文网:${url}"
echo $website1
echo $website2
xjh@ubuntu:~/iot/tmp$ source test.sh 
C语言中文网:${url}
C语言中文网:http://c.biancheng.net
xjh@ubuntu:~/iot/tmp$ 

(1)以单引号' '包围变量的值时,单引号里面是什么就输出什么,即使内容中有变量和命令(命令需要反引起来)也会把它们原样输出。这种方式比较适合定义显示纯字符串的情况,即不希望解析变量、命令等的场景。

(2)以双引号" "包围变量的值时,输出时会先解析里面的变量和命令,而不是把双引号中的变量名和命令原样输出。这种方式比较适合字符串中附带有变量和命令并且想将其解析后再输出的变量定义。

(3)如果需要原样输出时就加单引号;没有特别要求的字符串等最好都加上双引号。

5、命令替换

(1)Shell 支持将命令的执行结果赋值给变量,即所谓的“命令替换”。

(2)“命令替换”有两种写法:把命令用反引号包围起来、把命令用$()包围起来。

variable=`command`
variable=$(command) 

#注意,多条命令之间可以用分号隔开,见第(5)点的例子

(3)例如,在/iot/tmp目录中创建一个名为 log.txt 的文本文件,内容是“I am coding!”。然后使用 cat 命令将 log.txt 的内容读取出来,并赋值给一个变量,然后使用 echo 命令输出。

xjh@ubuntu:~/iot/tmp$ cat log.txt 
I am coding!
xjh@ubuntu:~/iot/tmp$ log=$(cat log.txt) | echo ${log}
I am coding!
xjh@ubuntu:~/iot/tmp$ log=`cat log.txt` | echo ${log}
I am coding!
xjh@ubuntu:~/iot/tmp$ 

(4)例如,date 命令用来获得当前的系统时间,使用命令替换可以将它的结果赋值给一个变量。

#!/bin/bash

begin_time=`date`    #开始时间,使用``替换
sleep 20s            #休眠20秒
finish_time=$(date)  #结束时间,使用$()替换

echo "Begin time: $begin_time"
echo "Finish time: $finish_time"

运行脚本,20 秒后可以看到输出结果:

Begin time: 星期五 二月 24 17:15:48 CST 2023
Finish time: 星期五 二月 24 17:16:08 CST 2023

使用 data 命令的%s格式控制符可以得到当前的 UNIX 时间戳,这样就可以直接计算脚本的运行时间了。UNIX 时间戳是指从 1970 年 1 月 1 日 00:00:00 到目前为止的秒数。

#!/bin/bash

begin_time=`date +%s`    #开始时间,使用``替换
sleep 20s                #休眠20秒
finish_time=$(date +%s)  #结束时间,使用$()替换

run_time=$((finish_time - begin_time))  #时间差
#代码中的 (( ))是 Shell 数学计算命令之一。
#在 Shell 中进行数据计算不那么方便,必须使用专门的数学计算命令。

echo "begin time: $begin_time"
echo "finish time: $finish_time"
echo "run time: ${run_time}s"

运行脚本,20 秒后可以看到输出结果:

begin time: 1555639864
finish time: 1555639884
run time: 20s

(5)再举一个例子:

xjh@ubuntu:~/iot/tmp$ cat test.sh 
#!/bin/bash

DATE=`date`
echo "Date is $DATE"

USERS=`who | wc -l`
echo "Logged in user are $USERS"

UP=`date ; uptime`
echo "Uptime is $UP"
xjh@ubuntu:~/iot/tmp$ ./test.sh 
Date is 星期日 二月 26 14:41:37 CST 2023
Logged in user are 2
Uptime is 星期日 二月 26 14:41:37 CST 2023
 14:41:37 up 10:46,  2 users,  load average: 0.07, 0.03, 0.05
xjh@ubuntu:~/iot/tmp$ 

(6)如果命令的输出内容包括多行(也即有换行符),或者含有多个连续的空白符,那么在输出变量时应该将变量用双引号包围,否则系统会使用默认的空白符来填充,这会导致换行无效,以及连续的空白符被压缩成一个,如下所示。因此为了防止出现格式混乱的情况,建议在输出变量时加上双引号。

#!/bin/bash

LSL=`ls -l`
echo $LSL  #不使用双引号包围
echo "--------------------------"  #输出分隔符
echo "$LSL"  #使用引号包围
xjh@ubuntu:~/iot/tmp$ ls
test1.sh  test2.sh  test.sh
xjh@ubuntu:~/iot/tmp$ . test2.sh 
total 12 -rw-rw-r-- 1 xjh xjh 126 二月 24 17:19 test1.sh -rw-rw-r-- 1 xjh xjh 147 二月 24 17:25 test2.sh -rw-rw-r-- 1 xjh xjh 230 二月 24 17:15 test.sh
--------------------------
total 12
-rw-rw-r-- 1 xjh xjh 126 二月 24 17:19 test1.sh
-rw-rw-r-- 1 xjh xjh 147 二月 24 17:25 test2.sh
-rw-rw-r-- 1 xjh xjh 230 二月 24 17:15 test.sh
xjh@ubuntu:~/iot/tmp$  

(7)原则上第(2)点中提到的两种变量替换的形式是等价的,可以随意使用;但是反引号毕竟看起来像单引号,有时候会对查看代码造成困扰,而使用 $() 就相对清晰,能有效避免这种混乱。而且有些情况必须使用 $(),比如$() 支持嵌套,反引号不行。下面的例子演示了使用计算 ls 命令列出的第一个文件的行数,这里使用了两层嵌套。

xjh@ubuntu:~/iot/tmp$ ls
test1.sh  test2.sh  test.sh
xjh@ubuntu:~/iot/tmp$
xjh@ubuntu:~/iot/tmp$ Fir_File_Lines=$(wc -l $(ls | sed -n '1p'))
xjh@ubuntu:~/iot/tmp$ echo "$Fir_File_Lines"
9 test1.sh
xjh@ubuntu:~/iot/tmp$

要注意的是,$() 仅在 Bash Shell 中有效,而反引号可在多种 Shell 中使用。所以这两种命令替换的方式各有特点,究竟选用哪种方式全看个人需求。

6、只读变量 

(1)使用 readonly 命令可以将变量定义为只读变量,只读变量的值不能被改变。

(2)下面的例子尝试更改只读变量,结果报错:/bin/sh: NAME: This variable is read only.

#!/bin/bash

myUrl="http://see.xidian.edu.cn/cpp/shell/"
readonly myUrl
myUrl="http://see.xidian.edu.cn/cpp/danpianji/"

7、删除变量

(1)使用 unset 命令可以删除变量。

(2)unset 命令不能删除只读变量。

(3)变量被删除后不能再次使用,比如下面的脚本没有任何输出。

#!/bin/sh

myUrl="http://see.xidian.edu.cn/cpp/u/xitong/"
unset myUrl
echo $myUrl

二、Shell变量的作用域

(1)Shell 变量的作用域,指的是可以使用这个Shell 变量的范围。

(2)在不同  的作用域中,同名的变量不会相互干涉,就好像 A 班有个叫小明的同学,B 班也有个叫小明的同学,虽然他们都叫小明(对应于变量名),但是由于所在的班级(对应于作用域)不同,所以不会造成混乱。但是如果同一个班级中有两个叫小明的同学,就必须用类似于“大小明”、“小小明”这样的命名来区分他们。

(3)Shell 变量的作用域可以分为三种:

  • 有的变量只能在函数内部使用,这叫做局部变量;
  • 有的变量可以在当前 Shell 进程中使用,这叫做全局变量;
  • 有的变量可以在子进程(shell所启动的程序)中使用,这叫做环境变量。

1、Shell局部变量

(1)Shell 也支持自定义函数,但是 Shell 函数和 C++、Java、C# 等其他编程语言函数的一个不同点就是:在 Shell 函数中定义的变量默认也是全局变量,它和在函数外部定义变量拥有一样的效果。

(2)下面的代码输出结果是“99”。a 是在函数内部定义的,但是在函数外部也可以得到它的值,证明它的作用域是全局的,而不是仅限于函数内部。

#!/bin/bash

#定义函数
function func(){
    a=99
}

#调用函数
func

#输出函数内部的变量
echo $a

(3)要想让变量的作用域仅限于函数内部,在定义时加上local命令。比如下面代码的输出结果为空,这说明变量 a 在函数外部无效,是一个局部变量。

#!/bin/bash

#定义函数
function func(){
    local a=99
}

#调用函数
func

#输出函数内部的变量
echo $a

2、Shell全局变量

(1)所谓全局变量,就是指变量在当前的整个 Shell 进程中都有效。每个 Shell 进程都有自己的作用域,彼此之间互不影响。

(2)这里演示全局变量在不同 Shell 进程中的互不相关性。首先在桌面环境下打开两个shell,然后在其中一个shell窗口中定义一个变量 a 并赋值为 99,然后打印,在该shell中可以正确打印变量 a 的值;但是在另外一个shell窗口打印变量 a 的值,结果却为空。这说明全局变量 a 仅仅在定义它的第一个 Shell 进程中有效,对新的 Shell 进程没有影响。这很好理解,就像小王家和小徐家都有一部电视机(变量名相同),但是同一时刻小王家和小徐家的电视中播放的节目可以是不同的(变量值不同)。

(3)注意,全局变量的作用范围是当前的 Shell 进程,而不是当前的 Shell 脚本文件,它们是不同的概念。打开一个 Shell 窗口就创建了一个 Shell 进程,打开多个 Shell 窗口就创建了多个 Shell 进程,每个 Shell 进程都是独立的,拥有不同的进程 ID。在一个 Shell 进程中可以使用 source 命令执行多个 Shell 脚本文件,此时全局变量在这些脚本文件中都有效(因为使用source明命令执行脚本时还是在同一个进程中,如果用其他方式执行则创建了新进程,此时该全局变量在这些脚本中是无效的)。

3、Shell环境变量

(1)全局变量只在当前 Shell 进程中有效,对于其它 Shell 进程(另外打开的shell窗口)和子进程(比如利用bash命令创建的子进程)都无效。如果使用export命令将全局变量导出,那么它就在所有的子进程中也有效了,这称为“环境变量”。

(2)环境变量被创建时所处的 Shell 进程称为父进程,如果在父进程中再创建一个新的进程来执行 Shell 命令,那么这个新的进程被称作 Shell 子进程。当 Shell 子进程产生时,它会继承父进程的环境变量为自己所用,所以说环境变量可从父进程传给子进程。不难理解,环境变量还可以传递给孙进程。注意,两个没有父子关系的 Shell 进程是不能传递环境变量的,并且环境变量只能向下传递而不能向上传递,即“传子不传父”。

(3)创建 Shell 子进程最简单的方式是运行 bash 命令:

xjh@ubuntu:~/iot/tmp$ echo $$
3512
xjh@ubuntu:~/iot/tmp$ bash
xjh@ubuntu:~/iot/tmp$ #这看似没什么变化,但已经进入shell子程序
xjh@ubuntu:~/iot/tmp$ echo $$
3697
xjh@ubuntu:~/iot/tmp$ exit #利用exit退出shell子程序
exit
xjh@ubuntu:~/iot/tmp$ echo $$
3512
xjh@ubuntu:~/iot/tmp$

 (4)下面演示一下环境变量的使用:

xjh@ubuntu:~/iot/tmp$ a=22
xjh@ubuntu:~/iot/tmp$ echo $a
22
xjh@ubuntu:~/iot/tmp$ bash #创建了一个子进程
xjh@ubuntu:~/iot/tmp$ echo $a #默认情况下a 在 Shell 子进程中是无效的

xjh@ubuntu:~/iot/tmp$ exit #退出了子进程,返回到了父进程
exit
xjh@ubuntu:~/iot/tmp$ export a #使用 export 将 a 导出为环境变量
xjh@ubuntu:~/iot/tmp$ bash
xjh@ubuntu:~/iot/tmp$ echo $a  #在子进程中就可以使用了
22
xjh@ubuntu:~/iot/tmp$ exit
exit
xjh@ubuntu:~/iot/tmp$ 

(5)通过 export 导出的环境变量只对当前 Shell 进程以及所有的子进程有效,如果最顶层的父进程被关闭,那么环境变量也就随之消失,其它的进程也就无法使用了,所以说环境变量也是临时的。如果想让一个变量在所有 Shell 进程中都有效,需要将该变量写入 Shell 配置文件,比如将“export a=2”写进shell配置文件中(因为Shell 进程每次启动时都会执行配置文件中的代码做一些初始化工作,如果将变量放在配置文件中,那么每次启动进程都会定义这个变量。这部分内容见博文Shell配置脚本的加载与编写_天糊土的博客-CSDN博客)。

三、Shell特殊变量

1、概念与内容

某些(包含着其他字符的)变量有特殊含义,这样的变量被称为特殊变量。包括以下内容:

变量含义
$0表示当前脚本的文件名。
$n(n大于等于1)表示传递给脚本或者函数的参数,n表示第几个参数,例如,第一个参数是 $1,第二个参数是 $2。或者叫命令行参数、位置参数
$#表示传递给脚本或者函数的参数的个数。
$*表示传递给脚本或者函数的所有参数。
$@表示传递给脚本或者函数的所有参数。当被双引号" "包含时,$@ 与 $* 稍有不同。
$?表示上个命令的退出状态,或函数的返回值。
$$表示当前shell进程的进程号。对于shell脚本,就是这些脚本所在的进程的进程号。

2、位置参数(或者叫命令行参数):$n

(1)运行 Shell 脚本文件时我们可以给它传递一些参数,这些参数在脚本文件内部可以使用$n的形式来接收。同样,在调用函数时也可以传递参数。Shell 函数在定义时不能带参数,但是在调用函数时却可以传递参数,这些传递进来的参数,在函数内部就也使用$n的形式接收,例如,$1 表示第一个参数,$2 表示第二个参数,依次类推。这种通过$n的形式来接收的参数,在 Shell 中称为位置参数。

(2)我们知道,变量的名字必须以字母或者下划线开头,不能以数字开头;但是位置参数却偏偏是数字,这和变量的命名规则是相悖的,所以我们将它们视为“特殊变量”。 

(3)给脚本传递参数的实例:

xjh@ubuntu:~/iot/tmp$ cat test.sh
#!/bin/bash

echo "Language: $1"
echo "URL: $2"
xjh@ubuntu:~/iot/tmp$ ./test.sh C语言 http://www.baidu.com.cn
Language: C语言
URL: http://www.baidu.com.cn
xjh@ubuntu:~/iot/tmp$

(4)给函数传递参数的实例:

xjh@ubuntu:~/iot/tmp$ cat test.sh 
#!/bin/bash

function fun(){
   echo "Language:$1"
   echo "URL:$2"
}

fun c语言 http://www.baidu.com.cn

xjh@ubuntu:~/iot/tmp$ ./test.sh 
Language:c语言
URL:http://www.baidu.com.cn
xjh@ubuntu:~/iot/tmp$ 

(5)如果参数达到或者超过10 个,那么就得用${n}的形式来接收,例如 ${10}、${23}。花括号的作用是为了帮助解释器识别参数的边界,这跟使用变量时加{ }是一样的效果。 

3、其他特殊变量的实例

xjh@ubuntu:~/iot/tmp$ cat test.sh 
#!/bin/bash

echo $$
echo 'exe_File Name $0': $0   
#或者 echo "exe_File Name \$0 : $0"
echo 'First Parameter $1' : $1
echo 'Second Parameter $2' : $2
echo 'Quoted Values $@': $@
echo 'Quoted Values $*': $*
echo 'Total Number of Parameters $#': $#
xjh@ubuntu:~/iot/tmp$ echo $$
3775
xjh@ubuntu:~/iot/tmp$ source test.sh aa bb cc dd
3775
exe_File Name $0: bash
First Parameter $1 : aa
Second Parameter $2 : bb
Quoted Values $@: aa bb cc dd
Quoted Values $*: aa bb cc dd
Total Number of Parameters $#: 4
xjh@ubuntu:~/iot/tmp$ ./test.sh aa bb cc dd
4106
exe_File Name $0: ./test.sh
First Parameter $1 : aa
Second Parameter $2 : bb
Quoted Values $@: aa bb cc dd
Quoted Values $*: aa bb cc dd
Total Number of Parameters $#: 4
xjh@ubuntu:~/iot/tmp$

4、$*与$@的区别

(1)$* 和 $@ 都表示传递给函数或脚本的所有参数。当不被双引号包含时,都以"$1" "$2" … "$n" 的形式输出所有参数,当它们被双引号包含时(即"$*"、"$@"),"$*" 会将所有的参数作为一个整体,以"$1 $2 … $n"的形式输出所有参数,而"$@" 会将各个参数分开,以"$1" "$2" … "$n" 的形式输出所有参数。

(2)实例如下,从执行“./test.sh "a" "b" "c" "d" ”的结果可知加双引号包围的差异。

xjh@ubuntu:~/iot/tmp$ cat test.sh 
#!/bin/bash
echo "\$*=" $*
echo "\"\$*\"=" "$*"

echo "\$@=" $@
echo "\"\$@\"=" "$@"

echo "print each param from \$*"
for var in $*
do
    echo "$var"
done

echo "print each param from \$@"
for var in $@
do
    echo "$var"
done

echo "print each param from \"\$*\""
for var in "$*"
do
    echo "$var"
done

echo "print each param from \"\$@\""
for var in "$@"
do
    echo "$var"
done
xjh@ubuntu:~/iot/tmp$ ./test.sh a b c d
$*= a b c d
"$*"= a b c d
$@= a b c d
"$@"= a b c d
print each param from $*
a
b
c
d
print each param from $@
a
b
c
d
print each param from "$*"
a b c d
print each param from "$@"
a
b
c
d
xjh@ubuntu:~/iot/tmp$

(3)反斜杠“\”的作用:告诉解释器不要处理或者替代\后面所接的字符,它是怎样的你就给我怎样输出就好;不过\后面接某字母时,又表示一些特殊的含义,比如\r表示回车。 

5、获取上一个命令的退出状态,或者函数返回值:$? 

(1)$? 可以获取上一个命令的退出状态。所谓退出状态,就是上一个命令执行后的返回结果。退出状态是一个数字,大部分命令执行成功会返回 0,失败返回 1。不过也有一些命令返回其他值,以表示不同类型的错误。

xjh@ubuntu:~/iot/tmp$ echo $$
4200
xjh@ubuntu:~/iot/tmp$ cat test.sh 
#!/bin/bash
echo $$
if [ "$1" == 100 ]
then
   exit 0  #参数正确,退出状态为0
else
   exit 1  #参数错误,退出状态1
fi

#上面代码用了exit,因此这里需要在新进程中运行 test.sh,即以./test.sh方式运行
#否则当前Shell会话会被关闭,我们就无法取得它的退出状态了
xjh@ubuntu:~/iot/tmp$ ./test.sh 100 
4230  #可见创建了一个新的进程
xjh@ubuntu:~/iot/tmp$ echo $? #获取上一个命令的退出状态
0
xjh@ubuntu:~/iot/tmp$ echo $$
4200  #exit退回到父进程
xjh@ubuntu:~/iot/tmp$ bash test.sh 89
4236
xjh@ubuntu:~/iot/tmp$ echo $?
1
xjh@ubuntu:~/iot/tmp$ 

(2)严格来说,Shell 函数中的 return 关键字用来表示函数的退出状态,而不是函数的返回值;Shell 不像其它编程语言,没有专门处理返回值的关键字。

xjh@ubuntu:~/iot/tmp$ cat test.sh 
#!/bin/bash

#得到两个数相加的和
function add(){
    return `expr $1 + $2`
}

add 23 50  #调用函数
echo $?  #获取函数返回值
xjh@ubuntu:~/iot/tmp$ ./test.sh 
73
xjh@ubuntu:~/iot/tmp$ 
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

天糊土

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

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

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

打赏作者

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

抵扣说明:

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

余额充值