在Linux平台,解决问题需要理解项目的相关脚本,这就需要理解和掌握shell语法;掌握shell语法,也是自动化构建系统实现的必备知识。对于完整的shell语法,是不可能一篇文章讲完,甚至一本书也远远不够。shell是一个作为用户与Linux系统间接口的程序,它允许用户向操作系统输入需要执行的命令。这不仅是一套指令语法,还包含扩展的程序和命令功能,只能依靠日常使用中积累掌握。作为shell语法的总结,本文也参考如下资料进行整理,可以配套学习了解。
在Linux日常使用中,大都是直接通过命令来完成文件创建、下载、解压、处理、删除等工作。不过在维护Linux平台时,指令操作往往多且繁杂;为了统一处理这类问题,Linux下环境支持一整套shell语法来实现流程化操作;类似windows下的BAT和PowerShell语法,可以通过脚本来管理日常任务。shell语法包含指令和脚本语法操作,其中指令可以参考之前shell指令介绍。
脚本语法则在本节进行说明,首先描述下shell内部的一些特殊说明。
下面进一步进行相应的语法说明。这里以常用的脚本语法进行总结, 目录如下所示。
$n, read)$?, exit)对于shell来说,同时存在环境变量和局部变量,具体说明如下。
系统环境变量是在操作系统中全局存在的变量,为系统和应用程序提供了重要的配置信息和运行环境。例如命令和系统库检索查找的地址,用户工作目录等。在命令行或者脚本中,可通过$name的形式获取环境变量的具体值,也可通过echo $name打印环境变量的值。当然也可以通过printenv可以查看当前用户下的所有环境变量,或printenv $name打印具体信息。
环境变量也分为服务于文件系统的变量和用户自定义变量,其中系统中主要的环境变量如下所示。
| 变量名 | 描述 |
|---|---|
$UID |
当前账号的UID号 |
$USER |
当前账号的用户名称 |
$HISTSIZE |
当前终端的最大历史命令数目 |
$HOME |
当前账户的根目录 |
$LANG |
当前环境使用的语言 |
$PATH |
命令搜索目录 |
$PWD |
当前工作目录 |
$RANDOM |
随机返回0到32767的树 |
$PS1 |
命令提示符, 给出当前的用户名,机器名和当前目录名以及$符合 |
$PS2 |
二级命令提示符, 用于提示后续的输入,通常是>字符 |
对于用户环境变量,可通过export命令导入通过命令行或者shell脚本导入,具体如下所示。
# 导入USER_VAR变量到系统中, 只在当次环境有效
# 希望一直有效需要添加到用户启动脚本或者环境中,如/etc/profile、/etc/environment、.bashrc等
export USER_VAR="user test"
echo $USER_VAR
对于变量内容,一般为引号包裹的字符串,其中引号类型可以是双引号(““)或单引号(‘’)。其中双引号(““)表示一个整体,单引号(‘‘)则在此基础上还会屏蔽特殊符号。对于字符串中的引号还包含反引号(``),表示执行命令。具体如下所示。
# 字符串声明
test="test string"
echo "$test" # 输出test string
echo '$test' # 输出$test, $不识别为特殊符号
TEXT=' $HOME `date` \n'
echo "$TEXT" # $HOME `date` \n
# 反引号等同于$(cmd),执行内部cmd的命令
DATE=`date`
echo "$DATE" # Wed Oct 22 10:40:34 AM CST 2025
对于Linux中,另一个重要的部分就是局部变量,对于局部变量的操作,如下所示。
name="value"的格式,其中=号两边不能有空格。local var="value"的格式,local只能在函数内部使用,且只能被内部访问。readonly var="value"的格式,readonly的变量只能被定义,不能被修改。unset var的格式,删除后续不能够访问,不过只能用于删除非只读变量。echo $var或者echo ${var}进行读取,其中${var}表示使用变量var的值,如果不引起歧义,可以省略花括号。var="str1""str2"、var="str1"str2"str3",同时支持单引号模式。${#var}的语法可以获取变量的长度。${var:offset:length}的语法可以获取变量的子串,其中offset表示子串的起始位置,length表示子串的长度(可缺省,表示取所有)。具体示例如下所示。
var="test"
echo $var # 输出test
echo ${var} # 输出test
function test()
{
local var="local test"
echo $var # 输出local test
}
readonly r_var="readonly test"
echo $r_var # 输出readonly test
unset var
echo $var # 输出为空
# 字符串拼接("")
varstr_0="This is a connect string"
varstr_1="This"" is a connect string"
varstr_2="This" is "a connect string"
echo $varstr_0
echo $varstr_1
echo $varstr_2
# 字符串拼接('')
varstr_3='This is a connect string'
varstr_4='This'' is a connect string'
varstr_5='This 'is' a connect string'
echo $varstr_3
echo $varstr_4
echo $varstr_5
# 获取字符串长度
echo ${#varstr_0}
# 获取字符串子串
echo ${varstr_0:0:5}
echo ${varstr_0:1:4}
echo ${varstr_0:5}
bash中支持数组变量,其格式为array=(value1 value2 ...)的一维数组。另外可以通过array[index]=value来修改数组元素,如果数组不存在,则会创建该数组对应元素
echo ${array[index]}格式访问数组元素。echo ${array0[*]}和$echo {array[@]}格式访问数组所有元素。echo ${#array[*]}、echo ${#array[@]}和echo ${#array}可以获取数组的元素个数echo ${#array[0])获取数组对应元素长度具体示例如下所示。
# 定义数组array0和array1
array0=(A B C D)
array1[0]=A
array1[1]=B
# 访问关联数组的元素${array[index]}格式
echo ${array0[0]}
# 获取数组内的所有元素
echo ${array0[*]}
echo ${array0[@]}
# 获取数组的长度
echo ${#array0[*]}
echo ${#array0[@]}
echo ${#array0}
echo ${#array0[0]}
# 计算fibero数列
num=12
fibo=(1 1)
for ((i=2; i<$num; i++))
do
let fibo[$i]=fibo[$i-1]+fibo[$i-2]
done
echo ${fibo[@]}
对于bash shell来说,还支持关联数组,类似字典。关联数组的格式为declare -A array=([key]=value [key]=value ...)。
array[key]=value来给关联数组赋值。echo ${array[key]}格式访问关联数组元素。具体示例如下所示。
# 定义关联数组
declare -A site0=(["google"]="www.google.com" ["taobao"]="www.taobao.com")
declare -A site1
site1["google"]="www.google.com"
site1["taobao"]]="www.taobao.com"
# 访问关联数组内的元素
echo ${site0["google"]}
# 获取关联数组内的所有元素
echo ${site0[*]}
echo ${site0[@]}
# 获取关联数组的所有键值
echo ${!site0[*]}
echo ${!site0[@]}
# 获取关联数组的长度
echo ${#site0[*]}
echo ${#site0[@]}
echo ${#site0["google"]}
shell支持printf格式化输出,和echo相比,默认不添加’\n’,可以实现C语言类似的输出功能。
printf的格式如下所示: printf "format-string" [arguments...],同时和C语言类型,支持如下转义符。
具体示例如下所示。
# printf
printf "int: %d\n" 123 # 输出int: 123
printf "float: %.2f\n" 123.456 # 输出float: 123.46
printf "char: %c\n" a # 输出char: a
printf "string: %-10s\n" "hello world" # 输出string: hello world
printf "string: %s\n" "hello world" # 输出string: hello world
对于shell文件支持注释,主要包含块注释和单行注释。
<<[cmd].....[cmd]的形式,其中cmd可以是任意字符串,且结尾字符串需要保持一致。具体示例如下所示。
# 块注释,<<后可以使用任何字符串,但结尾字符串需要保持一致
<<comment
这是块注释
comment
# 单行注释
# ......
函数是封装可以被调用的命令行表达式,具体格式如下。
# 函数定义
function [func_name]()
{
# 函数体
}
对于函数参数,传递方式如下所示。
| 符号 | 功能 |
|---|---|
$0 |
当前脚本的文件名,$1表示第一个参数,$n表示第n个参数,依次类推。 |
$# |
传递给脚本或函数的参数个数。 |
$* |
以字符串显示所有向脚本传递的参数。如$*用「"」括起来的情况、以$1 $2 … $n的形式输出所有参数。 |
$@ |
与$*功能相同,使用时加引号。 |
$$ |
脚本运行的当前进程ID号。 |
$! |
后台允许的最后一个进程ID号。 |
$- |
Shell使用的当前选项,和set命令功能相同。 |
$? |
最后命令的退出状态,0表示没有错误,其他值表示有错误。 |
$1...${10}... |
当前输入参数($n)。 |
具体示例如下所示。
# 显示输入参数的函数
function show_info()
{
i=1
echo "var num:$#"
echo "var str:$*"
# 遍历输入参数
echo -n "show loop nums: "
while [ $i -le $# ]; do
# ${!i} 表示取第i个输入参数的值
echo -n "${!i} "
let i++
done
echo ""
}
show_info 4 5 6 7 8 9
# 获取传参的和
function sum_parameters()
{
local total=0
for num in "$@"; do
total=$((total + num))
done
echo "$total"
}
result=$(sum_parameters 1 2 3 4 5)
echo "result: $result"
# fibo数列
function fibo()
{
local fibo[0]=1
local fibo[1]=1
for ((i=2; i<$1; i++)); do
fibo[i]=$((${fibo[i-1]} + ${fibo[i-2]}))
done
echo "${fibo[*]}"
}
fibo 10
在bash中,支持以下方式进行数学或者逻辑运算。
$(( statement )), 其中statement为数学运算表达式,两边需要有空格,不需要再将表达式里面的大小于符号转义。[[ statement ]] 或 $[[ statement ]], 其中statement为数学运算表达式(两边需要有空格),增加模式匹配特效。echo "scale=4; 3.44/5" | bc主要支持的数学符号如下所示。
| 符号 | 描述 | 举例说明 |
|---|---|---|
| +、-、*、/ | 加减乘除 | $((2+2)) |
| ++、– | 自增自减 | $((i++)) |
| ** | 乘方 | $((2**3)) |
| % | 取余 | $((2%3)) |
| = | 赋值 | $((a=2)) |
| 符号 | 描述 | 举例说明 |
|---|---|---|
-eq、== |
检查数是否相等 | [ $a -eq $b ] |
-ne、!= |
检查数是否不相等 | [ $a -ne $b ] |
-gt、> |
检查左边是否大于右边 | [ $a -gt $b ] |
-lt、< |
检查左边是否小于右边 | [ $a -lt $b ] |
-ge、>= |
检查左边是否大于等于右边 | [ $a -ge $b ] |
-le、<= |
检查左边是否小于等于右边 | [ $a -le $b ] |
| 符号 | 描述 | 举例说明 |
|---|---|---|
! |
非运算 | [ !false ] |
-o、\|\| |
或运算 | [ $a -eq $b -o!true ] |
-a、&& |
与运算 | [ $a -eq $b -a!true ] |
-z |
判断字符串是否为空,为0时返回true | [ -z " " ] |
-n |
检测字符串是否不为空,不为0时访问true | [ -n " " ] |
$ |
检查字符串是否为空,不为空则返回true | [ $var ] |
具体示例如下所示。
# 使用 $[] 或 $(( )) 进行数学运算
a=$((2+5-3*2))
echo "a:$a"
b=$((2**3 - 6/3))
echo "b:$b"
a=$((b++))
echo "a:$a b:$b"
a=$((b%a))
echo "a:$a b:$b"
c1=$[$[a==b] || $[a<b]]
c2=$[a<=b]
echo "c1:$c1 c2:$c2"
if [ $a -le $b ] || [ $a -eq $b ]; then
echo "a<=b"
fi
[ -f "err.log" ] && echo -n Y || echo -n N
echo -n " "
[ "err.log" -ef "err2.log" ] && echo -n Y || echo -n N
echo -n " "
[ "err.log" -nt "err2.log" ] && echo -n Y || echo -n N
echo -n " "
[ "err.log" -ot "err2.log" ] && echo -n Y || echo -n N
echo -n " "
[ "err.log" -ot "errNil.log" ] && echo -n Y || echo -n N
echo ""
# 使用 expr 命令进行数学运算
let c3=`expr $a + $b`
echo "c3:$c3"
# 使用 bc 命令进行复杂的数学运算
# scale=2 表示结果保留两位小数
result=$(echo "scale=2; 10/3" | bc)
echo "result: $result"
Linux平台命令支持与(&&)和或(||)执行,具体说明如下所示。
&&:and语句,满足前一个后续的才会执行。||:or语句,前一个不满足后一个才执行。具体示例如下所示。
# and语句执行,满足前一个后一个才执行
statement1 && statement2 && ... && statementX
# or语句执行,前一个不满足后一个才执行
statement1 || statement2 || ... || statementX
# 例程
# 跳转uboot目录,不成功则返回
cd "${PLATFORM_UBOOT_PATH}"/ || return
# 对于 && 和 ||,可以配合{}来构造语句块。
# 测试构造语句块输出
ls -alF && {
echo "block test"
ps -ef | grep ls
}
在bash中, 默认会将错误输出到屏幕中,此时使用输出重定向,可以将异常数据保存到其它路径下,如文件、管道符或者其它通讯接口中。
>重定向输出内容, 如果文件已经存在,此时会覆盖该文件重新生成(数据丢失)。>>在文件后追加数据,重定向输出内容。&>可以将标准输出和错误数据定向到同一文件。&>>可以追加数据的方式定向标准输出和错误数据。2>或2>>可以将错误数据输出重定向文件。2>&1。/dev/null是标准丢弃通道,如果数据不希望显示和处理,将其导入到/dev/null中。具体示例如下所示。
# 将date的输出写入testfile
date>test/testfile
# 将date的输出重定向追加到testfile后
date>>test/testfile
# 将标准输出和错误输出定向到不同文件
# 标准输出重定向不带数字
# 错误输出重定向使用格式:"cmd 2>err.file"
ls -l /etc/hosts /nofile > test/test.log 2> test/err.log
# 将标准输出和错误输出定向到同一文件(&>不追加,&>>追加)
ls -l /etc/hosts /nofile &> err2.log
# 将错误处理导向标准输入通道,从而写入标准输出文件
ls nofile > test/1.txt 2>&1
# 将信息导入到/dev/null中
ls nofile &> /dev/null
如果反过来希望文件被命令所访问,则使用输入重定向”<, «“。
命令 « 分隔符 …… 分隔符
# 将testfile用wc处理
wc<test/testfile
cat > test/test.file << EOF
this is a test!
this is the next lines!
EOF
Linux的管道,可以将命令的输出结果,存储到管道中,然后让后续命令从管道读取数据,进行进一步处理,对于管道,通过|连接。如果命令需要顺序执行,可以通过先写入文件,在读取文件访问,当然也可以优化为管道使用。使用管道命令流时不要重复使用相同的文件名(可能在开始时就被覆盖)。
具体格式:命令1 | 命令2
模型为: 命令1输出 => 写入管道 => 管道 => 读取管道 => 命令2输入
# 基于文件中转的命令
ls -alF>>test/testfile
sort<test/testfile
# 使用管道的连续命令
ls -alF | sort
# 显示当前进程,排序,然后分页显示
ps | sort | more
# 读取file1, 排序内容,删除重复后写入mydata.txt
cat test/file1.txt | sort | uniq > test/mydata.txt
在shell语法中,循环语句用于迭代,或者重复部分操作。对于循环来说,支持for、while和until三种方式,另外支持break命令来中止循环、continue命令来跳过当前循环、以及:命令来占位。详细说明如下所示。
for用于迭代列表中的项或者一系列数字;for语句的结构如下所示。
# for循环格式
# variable: 循环变量
# statemets: 循环体执行的命令
for variable in values
do
statements
done
具体示例如下所示。
# 遍历/etc/profile.d/的sh文件
for i in /etc/profile.d/*.sh; do
if [ -r $i ]; then
echo $i
fi
done
unset i
# 执行ls脚本
for file in $(ls f*.sh); do
lpr $file
done
# 遍历数组
fruits=("苹果" "香蕉" "柚子")
for fruit in "${fruits[@]}"; do
echo "i love ${fruit}"
if [ ${fruit} == "香蕉" ]; then
break
fi
done
# 遍历文件中读取的数据
for entry in $(cat /etc/passwd); do
echo ${entry}
done
# 使用for循环语句遍历数字
for ((i=1; i<10; i++))
do
echo $i
done
用于循环的while语句,和for类似,当不满足条件时,循环结束;while的格式如下所示。
# while循环格式
# condition: 循环条件
# statement: 循环体执行的命令
while condition; do
statements
done
具体示例如下所示。
# 遍历数据
count=1
while [ ${count} -le 3 ]; do
echo ${count}
count=$((count+1))
done
# 数学运算
i=1
sum=0
while [$i -le 10]; do
let sum = sum + $i
let i++
done
echo $i $sum
# 遍历文件
# 指定文件夹路径
folder="/home/program/download/tmp"
# 遍历文件夹中的文件
while IFS= read -rd '' file; do
echo "处理文件: $file"
# 在这里可以执行你需要的操作,对每个文件进行处理
done << (find "$folder" -type f -print0)
用于循环的until语句,当满足条件时,循环结束;其格式如下所示。
# until循环语句
# condition: 判断条件是否满足
# statement: 循环体执行的命令
until condition
do
statement
done
具体示例如下所示。
# 判断用户是否登录
until who | grep "$1" > /dev/null
do
sleep 1
done
# 遍历文件
count=0
until [ $count -ge 3 ]; do
echo -n "${count} "
let count++
done
分支语句,用于根据条件进行分支处理。是shell脚本常用的语句,如根据配置选项开启或者关闭某些功能执行,根据用户的输入控制脚本执行流程等。常见的分支语句有if、case两种,下面进行详细说明。
if可以用于根据条件进行分支处理,如检测某个脚本,库或目录不存在,执行其它分支。
对于if的格式如下所示。
if [ command ]; then
dothings
elif [ command ]; then # elif可多个,也可以不存在
dothings
else
dothings
fi
其中command主要包含如下部分.
# 文件或者目录判断
[ -a FILE ] 如果文件存在则为真
[ -b FILE ] 文件存在,且为块设备文件
[ -c FILE ] 文件存在,且为字符设备文件
[ -d FILE ] 如果文件存在且是一个目录则返回为真
[ -e FILE ] 如果指定的文件或目录存在时返回为真
[ -f FILE ] 如果文件存在且是一个普通文件则返回为真
[ -L FILE ] 判断文件存在且为软链接设备
[ -p FILE ] 判断文件存在且为命名管道
[ -r FILE ] 如果文件存在且是可读的则返回
[ -s FILE ] 文件存在且大小非空
[ -w FILE ] 如果文件存在且是可写的则返回为真
[ -x FILE ] 如果文件存在且是可执行的则返回为真
[ file1 -ef file2 ] 文件使用相同设备,inode编号则为真
[ file1 -nt file2 ] file1比file2更新,或仅file2不存在时为真
[ file1 -ot file2 ] file1比file2更旧,或仅file2不存在时为真
# 字符串判断
[ -z STRING ] 如果STRING的长度为零则返回为真,即空是真
[ -n STRING ] 如果STRING的长度非零则返回为真,即非空是真
[ STRING1 ] 如果字符串不为空则返回为真,与-n类似
[ STRING1 == STRING2 ] 如果两个字符串相同则返回为真
[ STRING1 != STRING2 ] 如果字符串不相同则返回为真
[ STRING1 < STRING2 ] 如果 “STRING1”字典排序在“STRING2”前面则返回为真。
[ STRING1 > STRING2 ] 如果 “STRING1”字典排序在“STRING2”后面则返回为真
# 数值判断
[ INT1 -eq INT2 ] INT1和INT2两数相等返回为真
[ INT1 -ne INT2 ] INT1和INT2两数不等返回为真
[ INT1 -gt INT2 ] INT1大于INT2返回为真
[ INT1 -ge INT2 ] INT1大于等于INT2返回为真
[ INT1 -lt INT2 ] INT1小于INT2返回为真
[ INT1 -le INT2 ] INT1小于等于INT2返回为真
# 逻辑判断
[ ! EXPR ] 逻辑非,如果 EXPR 是false则返回为真。
[ EXPR1 -a EXPR2 ] 逻辑与,如果 EXPR1 and EXPR2 全真则返回为真。
[ EXPR1 -o EXPR2 ] 逻辑或,如果 EXPR1 或者 EXPR2 为真则返回为真。
[ EXPR ] || [ EXPR ] 用OR来合并两个条件
[ EXPR ] && [ EXPR ] 用AND来合并两个条件
基于上述命令配合,下面展示一些常用的命令组合。
# 检测目录是否在PATH中
echo $PATH
if [ -d "$1" ] && echo $PATH | grep -E -q "(^|:)$1($|:)"; then
echo "directory $1 in PATH"
else
echo "directory $1 not in PATH"
fi
# 可以使用if test condition, 等同于 if [ condition ]
if test -f "file.c"; then
echo "file exist!"
fi
# 使用if...elif..else结构
read -p "timeofday" timeofday
if [ "$timeofday" == "0" ]; then
echo "morning"
elif [ "$timeofday" == "1" ]; then
echo "afternoon"
else
echo "sorry"
exit 1
fi
exit 0
case则主要用于分支判断语句,根据不同的内容执行不同的分支,格式为
# 分支语句格式
case variable in
pattern1)
command1
;;
pattern2)
command2
;;
#......
esac
# 执行不同的分支处理
case "$BUILD_OPT" in
"u-boot")
compile_uboot
;;
"kernel")
compile_kernel
;;
"rootfs")
compile_kernel
create_rootfs
;;
"image")
compile_uboot
compile_kernel
create_rootfs
do_pack
;;
*)
echo "sorry, input not valid"
;;
esac
shell脚本输入变量通过$n(n=1,2…)获取输入值
# $1, $2表示文件输入值
# 如 ./test.sh test1 test2
# 则 $1=test1, $2=test2
echo "read 1 value is $1"
echo "read 2 value is $2"
读取命令行输入的值使用read命令。
| 选项 | 功能 |
|---|---|
| -p | 显示提示信息 |
| -t | 设置读入数据的超时时间 |
| -n | 设置读取n个字符后结束,默认会读取标准输入的一整行内容 |
| -r | 支持读取\, 而默认read命令会将\理解为转义字符 |
| -s | 静默模式,不显示输入内容 |
| -t | 设置超时时间(单位s),超时自动退出 |
具体示例如下所示。
# 读取外部数据,存储到value中
read -p "read:" value;
# 超时等待5s输入用户名
read -t 5 -p "输入用户名:" user
read -s -p "输入密码:" passwd
useradd "$user"
echo $passwd | passwd --stdin "$user"
在shell脚本中,用$?表示执行的状态,可以用exit命令返回脚本执行的结果。
# 打印执行状态
if [[ $? -ne 0 ]]; then
read -p "shell faild, wheather exit?(y/n, default is y)" exit_val
if [ -z ${exit_val} ] || [ ${exit_val} == 'y' ]; then
exit 1
fi
fi
本文中从变量、函数、注释、计算、输出重定向、循环、条件分支等方面进行了介绍了Shell脚本的语法知识,可以看到与编程语言有很多相似之处,很多语法和概念都一样,可以参考配合学习。
不过了解了这些语法,并不表示就已经掌握,不信可以去查看大部分开源项目中shell脚本,理解起来还是很困难。这是因为如果一个项目比较小,shell脚本有些杀鸡用牛刀,往往不会使用。项目需要shell脚本管理时,就已经足够复杂了;此时shell脚本覆盖这些复杂度,往往就是一层层抽象;只是学习这些基础语法就去理解,相当于拿学生的知识理解教授级别的难题,中间差了几个档次,觉得shell脚本晦涩难以理解的原因正是在于此。那么有什么好办法呢?就是自己去实践构建些项目,从简单到复杂,基础就是简单的处理文件,执行编译、复制的操作,复杂一些可以构建测试、安装、配置、运行、打包、清理等SDK方案,只有多加练习,才能真正掌握shell脚本语法。
直接开始下一章节:嵌入式Linux平台软件的交叉编译。