build_embed_linux_system

Linux平台shell语法

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

这里以常用的脚本语法进行总结, 目录如下所示。

shell_grammer

在Linux日常使用中,大都是直接通过命令来完成文件夹创建,文件下载,处理,解压,删除等工作。不过在维护Linux平台时,这类指令操作往往多且繁杂,为了统一处理这类问题,Linux下的shell环境支持一整套shell语法来管理操作,类似windows下的bat和powershell语法,可以通过脚本来管理日常任务,这里列出我在使用Linux中涉及的语法。

在命令行中,使用双引号和单引号表示引用一个整体,另外单引号可以屏蔽转译字符。

注意:本文中的shell语法在/bin/bash环境下验证,对于嵌入式Linux环境可能因为使用不同的shell工具,并不一定支持全部的语法,需要注意。

env_path

可通过”$[name]”的形式获取环境变量的具体值,对于环境变量通过printenv可以查看当前用户下的环境变量。

变量名 描述
$UID 当前账号的UID号
$USER 当前账号的用户名称
$HISTSIZE 当前终端的最大历史命令数目
$HOME 当前账户的根目录
$LANG 当前环境使用的语言
$PATH 命令搜索目录
$PWD 当前工作目录
$RANDOM 随机返回0到32767的树
$PS1 命令提示符, 给出当前的用户名,机器名和当前目录名以及$符合
$PS2 二级命令提示符, 用于提示后续的输入,通常是>字符

execute

Linux平台命令支持与和或执行,分别如下。

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

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

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

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

comment

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

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

# 单行注释

function

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

function [name]()
{
    #函数体
}

对于函数参数,通过如下格式访问。

下面举例说明

function show_info()
{
    i=1
    echo "var num:$#"
    echo "var str:$*"
    while [ $i -le $# ]; do
        echo "${!i}"
        let i++
    done
}

show_info 4 5 6 7 8 9

variable

符号包含: 双引号(“”),单引号(‘’),反引号(``),转义符号(‘')。

  1. 双引号(““)表示一个整体
  2. 单引号(‘‘)也表示一个整体,此外还会屏蔽特殊符号
  3. 反引号(``)用于命令替换,执行内部命令,可以使用$(cmd替代)
  4. 转译符号(‘')表示具有特殊含义的符号,详细见echo命令

在linux系统中,可以直接使用var=xxx的形式声明变量,然后通过$var, ${var}进行读取。

path=/home/freedom

#输出path的值
echo $path
echo ${path}

#"",''都表示一个整体,''会屏蔽特殊符号
echo "$test"  #输出test变量
echo '$test'  #输出$test字符串
echo "当前账户: `who | awk '{print $1}'`"
echo "当前账户: $(who | awk '{print $1}')"

#定义变量只读,后续不能够修改
url="file"
readonly usl

#删除变量
uset url

shell中支持数组变量,如下定义。

#定义数组array0和array1
array0=(A B C D)
array1[0] = A
array2[1] = B

#访问关联数组的元素${array[index]}格式
${array0[0]}

#获取数组内的所有元素
echo ${array0[*]}
echo ${array0[@]}

#计算fiber
num=12
fibo=(1,1)
for ((i=2;i<$num;i++))
do
    let fibo[$i]=fibo[$i-1]+fibo[$i-2]
done
echo $fibo

shell支持关联数组,类似字典

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

math

在bash中,可以通过$((statement))进行数学运算

#通过$(())进行数学运算
val=$((2*3+6))
val0=$((2*5+(6-2)*3))

echo $val
echo $val0

#通过$[]进行数学运算
var1=100
var2=45
var3=$[ ${var1} / ${var2} ]
echo $var3

#通过bc命令进行运算
var4=$(echo " scale=4; 3.44/5" | bc)
echo ${var4}

#可以使用`command`格式执行命令
NUM=1
echo `expr $NUM + 1`

let x=1
let x++
echo $x

remap

在bash中, 默认会将错误输出到屏幕中,此时使用输出重定向,可以将异常数据保存到FALSA中。

  1. 使用”>”重定向输出内容, 如果文件已经存在,此时会覆盖该文件重新生成(数据丢失),
  2. 使用”»“在文件后追加数据,重定向输出内容。
  3. 使用”&>”可以将标准输出和错误数据定向到同一文件
  4. 使用”&»“可以追加数据的方式定向标准输出和错误数据
  5. 使用”2>”或”2»“可以将错误数据输出重定向文件
  6. 将错误数据通道重定向标准输出通道”2>&1”
  7. ‘/dev/null’是标准丢弃通道,如果数据不希望显示和处理,将其导入到/dev/null中
#将date的输出写入testfile
date>testfile

#将date的输出重定向追加到testfile后
date>>testfile

#将标准输出和错误输出定向到不同文件
#标准输出重定向不带数字
#错误输出重定向使用格式:"cmd 2>err.file"
ls -l /etc/hosts /nofile > test.log 2> err.log

#将标准输出和错误输出定向到同一文件(&>不追加,&>>追加)
ls -l /etc/hosts /nofile &> err2.log

#将错误处理导向标准输入通道,从而写入标准输出文件
ls nofile > 1.txt 2>&1

#将信息导入到/dev/null中
ls nofile &> /dev/null

如果反过来希望文件被命令所访问,则使用输入重定向”<, «“。

命令 « 分隔符 …… 分隔符

#将testfile用wc处理
wc<testfile

cat > test.file << EOF
this is a test!
this is the next lines!
EOF

pipe

Linux的管道,可以将命令的输出结果,存储到管道中,然后让后续命令从管道读取数据,进行进一步处理,对于管道,通过”|“连接。如果命令需要顺序执行,可以通过先写入文件,在读取文件访问,当然也可以优化为管道使用。使用管道命令流时不要重复使用相同的文件名(可能在开始时就被覆盖)。

命令1 | 命令2

模型为: 命令1输出 => 写入管道 => 管道 => 读取管道 => 命令2输入

# 基于文件中转的命令
ls -alF>>testfile
sort<testfile

# 使用管道的连续命令
ls -alF | sort

# 显示当前进程,排序,然后分页显示
ps | sort | more

# 读取file1, 排序内容,删除重复后写入mydata.txt
cat file1.txt | sort | uniq > mydata.txt

loop

在linux中,循环语句主要有for和while语句,其中for用于迭代列表中的项或者一系列数字。在循环中支持使用break中止当前循环,continue则中止某次循环,但不结束整个循环。在循环中支持语句来跳出循环或者占位。

  1. break命令,跳出当前循环
  2. :命令,空命令
  3. continue命令,跳到下一次循环

for语句结构如下所示。

for variable in values
do
    statements
done

for例程显示如下所示。

# for循环格式
for variable in list; do
    # 执行循环体的命令
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

用于循环的while语句,当不满足条件时,循环结束。

# while循环格式
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

for ((i=1; i<10; i++))
do 
    echo $i
done

# 遍历文件
# 指定文件夹路径  
folder="/home/program/download/tmp"  
  
# 遍历文件夹中的文件  
while IFS= read -rd '' file; do  
    echo "处理文件: $file"  
    # 在这里可以执行你需要的操作,对每个文件进行处理  
done < <(find "$folder" -type f -print0)

用于循环的until语句,当满足条件时,循环结束。

# until循环语句
until condition
do
    statement
done

# 判断用户是否登录
until who | grep "$1" > /dev/null
do
    sleep 60
done

branch

常见的分支语句有if和case,其中if可以用于判断,如检测某个脚本,库或目录不存在,执行其它分支,其格式如下.

# if [ command ]; then 
#       dothings
# elif [ command ]; then -- 可省略
#       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

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

next_chapter

返回目录

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