在 Shell 脚本语言的编程世界里,变量定义有着严格的规则。变量名只能由字母、数字以及下划线组成,并且变量名的开头绝对不能是数字。例如,user_name、age1、_count 都是合法的变量名,而 1user、name-1 则不符合规范。
Shell 脚本对变量名的大小写是严格区分的,Name 和 name 会被视为两个完全不同的变量。这就好比在现实生活中,大小写不同的两个名字代表着不同的个体一样。在实际编程中,为了提高代码的可读性,建议使用有意义的英文单词或词组来命名变量,比如用 file_path 表示文件路径,而不是使用像 a、b 这样无意义的字符组合。
定义变量的格式非常简洁,即 变量名=变量值。需要特别注意的是,等号两侧是严禁出现空格的。例如,path=/usr/local 是正确的写法,如果写成 path = /usr/local,Shell 解释器就会报错,因为它会把 path 当作一个命令,而把 = 和 /usr/local 当作命令的参数,这显然不是我们想要的结果。
另外,Shell 默认将所有值都视为字符串,这意味着在定义变量时,无需像其他编程语言那样显式声明变量的类型。比如,num=10 和 str="hello",在 Shell 中,num 虽然赋值为数字,但它依然被当作字符串存储,这一点与其他编程语言有所不同。
直接赋值:这是最常用的赋值方式,直接通过等号将值赋给变量。这种方式支持单引号、双引号和无引号三种形式。当值是纯文本且不含空格时,可以使用无引号形式,例如 path=/usr/local。如果值中包含空格或特殊字符,就需要使用引号将其括起来。单引号和双引号又有细微的差别,单引号用于原样输出,不会解析变量。比如,echo '变量$name不会被解析',输出结果就是 变量$name不会被解析。而双引号则允许变量解析,echo "当前用户是$USER",这里的 $USER 会被解析为当前登录用户的用户名并输出。
命令结果赋值:通过反引号()或$( )可以将命令的执行结果赋给变量。例如,current_time=$(date +”%Y-%m-%d %H:%M:%S”),这条命令会将系统当前时间按照指定的格式赋值给current_time变量。在实际使用中,更推荐使用$( )的形式,因为它比反引号更易嵌套,代码的可读性也更好。比如,当需要在一个命令结果中再嵌套其他命令时,使用$( )` 会更加清晰明了。
变量间赋值:将一个变量的值直接赋给另一个变量,如 alias=$name,赋值完成后,alias 与 name 存储的是相同的字符串。需要注意的是,后续如果修改 name 的值,alias 并不会自动更新,必须重新进行赋值操作才能使 alias 获得新的值。
自定义变量的作用域就像是一个小房间,仅限于当前的 Shell 进程。一旦脚本执行结束,这个变量就像房间里的物品被清空一样,会失效。它的定义方式非常简单,与普通变量的定义一致,比如我们定义一个变量来存储用户名:user="John",这里的user就是一个自定义变量。
如果想要删除自定义变量,可以通过unset命令来实现。例如,执行unset user后,变量user的值就会被清空,再次引用$user时,就会返回空值,就好像这个变量从未被定义过一样。
在实际的脚本编写中,自定义变量常用于临时存储中间计算结果。比如,当我们编写一个脚本来统计某个目录下文件的行数时,可以先使用自定义变量来存储文件路径,然后再进行后续的计算操作。假设我们要统计/var/log/syslog文件的行数,可以这样编写脚本:
file_path="/var/log/syslog"
line_count=$(wc -l < $file_path)
echo "文件 $file_path 的行数为: $line_count"
在这个例子中,file_path就是一个自定义变量,它临时存储了文件的路径,方便后续的命令使用。
环境变量的作用域就像是一个公共区域,通过export命令声明后,它就可以被子进程访问,就像在公共区域里的物品,所有子进程都可以使用。在系统中,有很多常见的系统环境变量,它们各自有着重要的作用。比如PATH,它是命令搜索路径,当我们在命令行输入一个命令时,系统会按照PATH中定义的目录顺序去查找对应的可执行文件。还有HOME,它表示用户主目录,我们可以使用cd $HOME命令快速回到用户的主目录。
如果用户想要自定义环境变量,同样需要通过export命令来定义。例如,我们定义一个环境变量MY_CONFIG,用来存储某个配置文件的路径:export MY_CONFIG=/etc/myapp/config.conf。这样,在当前 Shell 进程及其子进程中,都可以使用这个环境变量。
如果希望自定义的环境变量在系统重启后仍然生效,就需要将其写入到环境变量配置文件中。对于 bash shell 来说,通常可以写入~/.bashrc(针对当前用户)或/etc/profile(针对所有用户)文件中。写入后,使用source命令使配置立即生效,比如source ~/.bashrc。
在实际应用中,当我们编写一些需要跨脚本协作的工具时,环境变量就非常有用。比如,我们有一个主脚本和多个子脚本,它们都需要访问同一个配置文件,这时就可以通过环境变量来传递配置文件的路径。假设主脚本main.sh中定义了环境变量:
export CONFIG_PATH=/etc/myapp/config.conf
./sub_script.sh
在子脚本sub_script.sh中,就可以直接使用这个环境变量来读取配置文件:
config_file=$CONFIG_PATH
# 读取配置文件的操作
$1到$9分别对应第 1 到第 9 个参数,如果有 10 个以上的参数,就需要使用${10}、${11}等格式。例如,我们有一个脚本test.sh,执行命令./test.sh arg1 arg2,在脚本中,$1的值就是arg1,$2的值就是arg2。位置变量在处理脚本的输入参数时非常方便,比如我们可以根据不同的参数执行不同的操作。例如,编写一个简单的文件操作脚本:#!/bin/bash
if [ $# -lt 2 ]; then
echo "用法: $0 <操作> <文件名>"
exit 1
fi
operation=$1
file=$2
case $operation in
"create")
touch $file
echo "文件 $file 创建成功"
;;
"delete")
rm -f $file
echo "文件 $file 删除成功"
;;
*)
echo "不支持的操作: $operation"
;;
esac
在这个脚本中,$1用于接收操作类型(如create或delete),$2用于接收文件名,通过位置变量,脚本可以根据不同的参数执行相应的文件操作。
2. 特殊变量:特殊变量有着各自独特的用途。
$0表示脚本自身的名称。例如,在脚本中执行echo "脚本名:$0",如果脚本是通过./test.sh执行的,那么输出结果就是./test.sh。这个变量在脚本中用于获取脚本的名称,有时候我们可能需要根据脚本的名称来执行不同的逻辑。
$#表示传递给脚本的参数个数。这个变量在校验脚本的入参数量时非常有用。比如,我们可以使用if [ $# -lt 1 ]; then echo "请输入参数"; fi来判断是否有参数传入,如果参数个数小于 1,就提示用户输入参数。
$@和$*都表示所有参数,但它们之间有细微的差别。当不被双引号包含时,它们的表现是一样的,都会展开为所有参数。但是当被双引号包含时,$@会将每个参数当作独立的字符串处理,而$*会将所有参数视为一个整体字符串。在循环中遍历参数时,推荐使用$@,因为它可以更方便地处理每个独立的参数。例如:
#!/bin/bash
for arg in "$@"; do
echo "参数: $arg"
done
在这个例子中,使用$@可以依次输出每个独立的参数,而如果使用$*,在双引号的情况下,所有参数会被当作一个整体输出,无法实现逐个参数处理的效果。
在 Shell 脚本中,引用变量时,$符号是必不可少的关键标识。当我们定义了一个变量name="Alice",若要输出这个变量的值,就必须在变量名前加上$,即echo $name,这样才能正确输出Alice。如果省略了$,写成echo name,Shell解释器会将name视为普通字符串,直接输出name\,而不是变量的值。
当变量名与其他字符紧密连用时,就需要花括号{}来明确变量名的边界,这就好比给变量名划定了一个清晰的 “势力范围”。例如,定义num=10,若要输出numxy这样的字符串,其中num是变量,就必须使用花括号,写成echo "${num}xy",这样才能正确输出10xy。如果直接写成echo $numxy,Shell 会尝试解析numxy这个变量,而我们并没有定义过这个变量,所以会导致输出为空,从而引发错误。这种情况在实际编程中很容易被忽略,尤其是在处理复杂字符串拼接时,正确使用花括号可以有效避免这类解析错误,确保脚本的正确性和稳定性。
在 Shell 脚本中,引号的使用场景主要有三种,分别是无引号、单引号和双引号,它们各自有着独特的解析规则和适用场景。
| 引号类型 | 解析规则 | 典型场景 | 示例 |
|---|---|---|---|
| 无引号 | 保留空格,解析变量和命令 | 纯文本且含变量 | name="Bob"; echo Hello $name(输出Hello Bob) |
| 单引号(’) | 原样输出,不解析任何内容 | 固定字符串(含特殊字符) | echo '变量$name不会被解析,特殊字符!@#$也原样输出'(输出变量$name不会被解析,特殊字符!@#$也原样输出) |
| 双引号(”) | 解析变量和命令,保留空格 | 含变量或命令的字符串 | current_time=$(date +"%Y-%m-%d %H:%M:%S"); echo "当前时间是 $current_time"(输出当前时间) |
无引号的方式适用于纯文本且包含变量的场景,它会保留空格,并且能够解析变量和命令。例如,当我们定义message="world",执行echo Hello $message,输出结果为Hello world,这里的空格被保留,变量$message也被正确解析。
单引号的特点是所见即所得,它会将单引号内的所有内容都原样输出,不会对其中的变量、命令或特殊字符进行任何解析。比如,当我们执行echo '今天是 $(date +%Y-%m-%d)',输出的就是今天是 $(date +%Y-%m-%d),其中的$(date +%Y-%m-%d)命令并没有被执行,而是原样输出。这种特性在需要输出固定字符串,尤其是包含特殊字符,不想让它们被解析时非常有用。
双引号则相对灵活,它允许解析变量和命令,同时也会保留空格。当我们想要输出包含变量或命令执行结果的字符串时,双引号就派上用场了。例如,file_path="/etc/passwd"; line_count=$(wc -l < $file_path); echo "文件 $file_path 的行数为: $line_count",通过双引号,变量$file_path和命令执行结果$line_count都被正确解析并输出,同时字符串中的空格也被保留,使得输出结果符合我们的预期。
在 Shell 脚本中,我们可以通过readonly或declare -r来声明只读变量。这种变量一旦被赋值,就如同被上了一把锁,其值不可再被修改。只读变量通常用于定义一些在脚本执行过程中不会改变的常量,比如数学中的圆周率PI。我们可以这样定义:readonly PI=3.14159 或者 declare -r PI=3.14159 。之后,如果尝试修改PI的值,比如执行PI=3.14,Shell 会报错,提示该变量是只读的,无法修改,这就保证了常量的稳定性。
当我们不再需要某个变量时,可以使用unset命令来删除它。例如,我们定义了一个变量temp="test",如果后续不再使用它,就可以执行unset temp,这样temp变量就被删除了,再次引用$temp时,会返回空值。不过,需要注意的是,只读变量是无法被删除的。如果对只读变量执行unset操作,比如unset PI,同样会报错,因为只读变量的属性决定了它不能被随意修改或删除。
另外,在删除环境变量时,如果该变量是通过export声明的,需要先取消导出。例如,我们定义了一个环境变量export MY_ENV=value,如果要删除它,不能直接使用unset MY_ENV,而应该先执行unset -v MY_ENV(这里的-v选项表示删除变量),因为export只是声明变量的作用域为环境变量,变量本身还是可以通过unset命令来删除的。
索引数组:索引数组是一种常见的数据结构,它的下标从 0 开始。定义索引数组非常简单,使用数组名 =(值 1 值 2 值 3) 的形式即可。例如,我们要定义一个存储水果名称的数组,可以这样写:fruits=("apple" "banana" "orange") 。访问数组元素时,使用${数组名[下标]}的格式,比如要获取第一个水果的名称,执行echo ${fruits[0]},就会输出apple。如果想要获取数组中的所有元素,可以使用${数组名[@]},例如echo ${fruits[@]},会输出apple banana orange。而获取数组的长度,也就是元素的个数,则可以使用${#数组名[@]},对于fruits数组,${#fruits[@]}的值为 3。在实际应用中,索引数组常用于存储一组相同类型的数据,比如文件列表、用户 ID 列表等。例如,当我们需要批量处理一组文件时,可以将文件名存储在索引数组中,然后通过遍历数组来对每个文件进行操作。
关联数组:关联数组是一种更灵活的数据结构,它使用字符串作为键名,而不是数字下标。在使用关联数组之前,需要先使用declare -A声明数组名。例如,我们要记录学生的成绩,可以这样定义关联数组:declare -A scores; scores=([math]=90 [english]=85) 。访问关联数组的值时,使用${数组名[键名]}的形式,比如要获取数学成绩,执行echo ${scores[math]},会输出90。如果想要遍历关联数组的键名,可以使用${!数组名[@]},例如for subject in ${!scores[@]}; do echo $subject; done,会依次输出math和english。遍历值则使用${数组名[@]},如for score in ${scores[@]}; do echo $score; done,会依次输出90和85。关联数组非常适用于存储键值对数据,比如配置文件解析。在实际的项目中,我们可以将配置文件中的各项配置以键值对的形式存储在关联数组中,然后通过键名来快速获取和修改相应的配置值,大大提高了代码的可读性和可维护性。
在编写交互式脚本时,我们常常需要获取用户的输入信息,这就用到了read命令。read命令用于从标准输入(通常是键盘)读取数据,并将读取到的数据存储到变量中。例如,我们要编写一个简单的用户信息收集脚本:
#!/bin/bash
read -p "请输入您的姓名: " name
read -p "请输入您的年龄: " age
echo "您的姓名是 $name,年龄是 $age"
在这个脚本中,-p参数用于显示提示信息,引导用户输入相应的数据。read -p "请输入您的姓名: " name 这行代码会在终端显示 “请输入您的姓名:”,等待用户输入,用户输入的内容会被赋值给变量name。同样,age变量会获取用户输入的年龄。
为了确保输入的有效性,我们可以添加一些校验逻辑。比如,校验年龄是否为正整数:
#!/bin/bash
read -p "请输入您的姓名: " name
while true; do
read -p "请输入您的年龄: " age
if [[ $age =~ ^[0-9]+$ ]]; then
break
else
echo "输入的年龄无效,请输入一个正整数。"
fi
done
echo "您的姓名是 $name,年龄是 $age"
在这个改进后的脚本中,使用了while循环和正则表达式[[ $age =~ ^[0-9]+$ ]]来校验用户输入的年龄是否为正整数。如果输入无效,会提示用户重新输入,直到输入正确为止。这种交互式脚本在脚本初始化配置等场景中非常常见,通过与用户的交互,获取必要的配置信息,使脚本能够更好地适应不同的需求。
在处理文件时,经常需要逐行读取文件内容并进行相应的操作。利用while循环结合read命令,可以方便地实现这一功能。例如,我们有一个日志文件app.log,要统计其中包含 “error” 的行数:
#!/bin/bash
count=0
while read line; do
if [[ $line == *error* ]]; then
count=$((count + 1))
fi
done < app.log
echo "文件中包含'error'的行数为: $count"
在这个脚本中,while read line 会逐行读取app.log文件的内容,并将每行内容赋值给变量line。然后通过if语句判断当前行是否包含 “error”,如果包含,则将计数器count加 1。最后输出包含 “error” 的行数。
除了使用while循环,还可以使用for循环结合cat命令来逐行处理文件,但这种方式在处理大文件时可能会消耗较多的内存。更高效的逐行处理方式是使用awk命令,awk是一种强大的文本处理工具,它可以在读取文件时逐行进行处理,并且支持各种复杂的文本处理逻辑。例如,使用awk统计包含 “error” 的行数:
error_count=$(awk '/error/ {count++} END {print count}' app.log)
echo "文件中包含'error'的行数为: $error_count"
在这个awk命令中,/error/ 是一个模式,表示匹配包含 “error” 的行。当匹配到这样的行时,{count++} 会将计数器count加 1。END {print count} 表示在处理完整个文件后,输出计数器count的值,即包含 “error” 的行数。这种方式在处理大文件时效率更高,因为awk是逐行读取文件,而不是一次性将整个文件读入内存。
当编写接收多个参数的脚本时,参数校验是非常重要的一步,它可以确保用户正确调用脚本。通过$#可以判断参数的个数,例如:
#!/bin/bash
if [ $# -ne 2 ]; then
echo "用法: $0 <源目录> <目标目录>"
exit 1
fi
src_dir=$1
dest_dir=$2
# 后续处理逻辑,例如复制文件
cp -r $src_dir/* $dest_dir
在这个脚本中,首先通过if [ $# -ne 2 ]; then 判断用户输入的参数个数是否为 2。如果不是 2 个参数,就会输出正确的用法提示,并使用exit 1 退出脚本,返回错误状态码 1。如果参数个数正确,就将第一个参数赋值给src_dir,第二个参数赋值给dest_dir,用于后续的文件复制操作。
当需要处理复杂逻辑时,使用for循环结合$@ 可以遍历所有参数。例如,我们要编写一个批量重命名文件的脚本,给所有文件添加_backup后缀:
#!/bin/bash
for file in "$@"; do
new_name="${file}_backup"
mv "$file" "$new_name"
done
本文系作者在时代Java发表,未经许可,不得转载。
如有侵权,请联系nowjava@qq.com删除。