build_embed_linux_system

Linux平台shell语法说明

在Linux平台,解决问题需要理解项目的相关脚本,这就需要理解和掌握shell语法;掌握shell语法,也是自动化构建系统实现的必备知识。对于完整的shell语法,是不可能一篇文章讲完,甚至一本书也远远不够。shell是一个作为用户与Linux系统间接口的程序,它允许用户向操作系统输入需要执行的命令。这不仅是一套指令语法,还包含扩展的程序和命令功能,只能依靠日常使用中积累掌握。作为shell语法的总结,本文也参考如下资料进行整理,可以配套学习了解。

在Linux日常使用中,大都是直接通过命令来完成文件创建、下载、解压、处理、删除等工作。不过在维护Linux平台时,指令操作往往多且繁杂;为了统一处理这类问题,Linux下环境支持一整套shell语法来实现流程化操作;类似windows下的BAT和PowerShell语法,可以通过脚本来管理日常任务。shell语法包含指令和脚本语法操作,其中指令可以参考之前shell指令介绍。

脚本语法则在本节进行说明,首先描述下shell内部的一些特殊说明。

下面进一步进行相应的语法说明。这里以常用的脚本语法进行总结, 目录如下所示。

shell_var

对于shell来说,同时存在环境变量和局部变量,具体说明如下。

env_var

系统环境变量是在操作系统中全局存在的变量,为系统和应用程序提供了重要的配置信息和运行环境。例如命令和系统库检索查找的地址,用户工作目录等。在命令行或者脚本中,可通过$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

basic_var

对于Linux中,另一个重要的部分就是局部变量,对于局部变量的操作,如下所示。

具体示例如下所示。

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}

array_var

bash中支持数组变量,其格式为array=(value1 value2 ...)的一维数组。另外可以通过array[index]=value来修改数组元素,如果数组不存在,则会创建该数组对应元素

具体示例如下所示。

# 定义数组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[@]}

associative_array_var

对于bash shell来说,还支持关联数组,类似字典。关联数组的格式为declare -A array=([key]=value [key]=value ...)

具体示例如下所示。

# 定义关联数组
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"]}

printf

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

comment

对于shell文件支持注释,主要包含块注释和单行注释。

具体示例如下所示。

# 块注释,<<后可以使用任何字符串,但结尾字符串需要保持一致
<<comment
这是块注释
comment

# 单行注释
# ......

function

函数是封装可以被调用的命令行表达式,具体格式如下。

# 函数定义
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

math_logic

在bash中,支持以下方式进行数学或者逻辑运算。

主要支持的数学符号如下所示。

符号 描述 举例说明
+、-、*、/ 加减乘除 $((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平台命令支持与(&&)或(||)执行,具体说明如下所示。

  1. &&:and语句,满足前一个后续的才会执行。
  2. ||:or语句,前一个不满足后一个才执行。

具体示例如下所示。

# and语句执行,满足前一个后一个才执行
statement1 && statement2 && ... && statementX

# or语句执行,前一个不满足后一个才执行
statement1 || statement2 || ... || statementX

# 例程
# 跳转uboot目录,不成功则返回
cd "${PLATFORM_UBOOT_PATH}"/ || return

# 对于 && 和 ||,可以配合{}来构造语句块。
# 测试构造语句块输出
ls -alF && {
    echo "block test"
    ps -ef | grep ls
}

io_remap

在bash中, 默认会将错误输出到屏幕中,此时使用输出重定向,可以将异常数据保存到其它路径下,如文件、管道符或者其它通讯接口中。

具体示例如下所示。

# 将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

pipe

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

loop

在shell语法中,循环语句用于迭代,或者重复部分操作。对于循环来说,支持for、while和until三种方式,另外支持break命令来中止循环、continue命令来跳过当前循环、以及:命令来占位。详细说明如下所示。

for

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

用于循环的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语句,当满足条件时,循环结束;其格式如下所示。

# 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

branch

分支语句,用于根据条件进行分支处理。是shell脚本常用的语句,如根据配置选项开启或者关闭某些功能执行,根据用户的输入控制脚本执行流程等。常见的分支语句有if、case两种,下面进行详细说明。

if

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则主要用于分支判断语句,根据不同的内容执行不同的分支,格式为

# 分支语句格式
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

read

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"

exit

在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

summary

本文中从变量、函数、注释、计算、输出重定向、循环、条件分支等方面进行了介绍了Shell脚本的语法知识,可以看到与编程语言有很多相似之处,很多语法和概念都一样,可以参考配合学习。

不过了解了这些语法,并不表示就已经掌握,不信可以去查看大部分开源项目中shell脚本,理解起来还是很困难。这是因为如果一个项目比较小,shell脚本有些杀鸡用牛刀,往往不会使用。项目需要shell脚本管理时,就已经足够复杂了;此时shell脚本覆盖这些复杂度,往往就是一层层抽象;只是学习这些基础语法就去理解,相当于拿学生的知识理解教授级别的难题,中间差了几个档次,觉得shell脚本晦涩难以理解的原因正是在于此。那么有什么好办法呢?就是自己去实践构建些项目,从简单到复杂,基础就是简单的处理文件,执行编译、复制的操作,复杂一些可以构建测试、安装、配置、运行、打包、清理等SDK方案,只有多加练习,才能真正掌握shell脚本语法。

next_chapter

返回目录

直接开始下一章节:嵌入式Linux平台软件的交叉编译