搞过一阵子Linux和嵌入式开发的朋友,基本都绕不开两样东西:Shell和Cmake。一个是跟操作系统打交道的命令行环境,一个是用来自动化构建项目的工具。很多新手一开始容易懵,觉得这两个东西名字听着就很硬核,其实只要把基础概念理顺,把最常见的用法和坑摸清楚,日常开发会顺手非常多。
这篇博文我会把这两块放在一起聊,因为它们在实际项目里经常是配套出现的。你写一个build.sh脚本去调用cmake,用shell判断编译结果,再用cmake生成构建系统,最后由ninja或make把代码变成可执行文件,这套流程几乎是C/C++项目在Linux下最标准的打开方式。这篇文章适合刚接触Linux、想搞懂Shell脚本和Cmake的同学,也适合从Windows IDE转到命令行构建的嵌入式开发者。
1. Shell语言:先说清楚它到底是个什么
很多人把“Shell”和“终端”混在一起,其实终端只是一个窗口,真正的Shell是跑在窗口里的那个解释器程序。你敲的每一行命令,都是交给它去解析、执行,然后把结果返回给你。它本质上是用户和操作系统内核之间的一层壳,这也是“Shell”这个名字的由来。
1.1 常见的Shell解释器有哪些
Linux下常见的Shell有bash、zsh、sh、dash,有的发行版默认把/bin/sh软链接到dash,有的链接到bash,这会导致某些语法在脚本里表现不一致。比如dash不支持[[ ]]这种高级条件判断,你在Ubuntu上写#!/bin/sh却用了[[ ... ]],很可能会报错。
所以我的建议很直接:脚本第一行写#!/bin/bash,尽量不要用#!/bin/sh。bash是Linux上最通用的解释器,默认支持数组、正则、[[ ]]判断这些特性,兼容性比sh好得多。macOS虽然系统自带的是zsh,但bash也是预装的,写#!/bin/bash一样能跑。
查看当前系统用的什么Shell,执行:
bash复制echo $SHELL
如果你想临时换一个解释器,直接在终端输入对应名称即可,比如输zsh就切换到zsh,输入exit就退回原来的Shell。
1.2 脚本文件为什么要加执行权限
很多人第一次写脚本会这样:
bash复制vim test.sh
chmod +x test.sh
./test.sh
chmod +x是给脚本加可执行权限,不加的话你直接./test.sh会提示Permission denied。但如果你用bash test.sh这种方式执行,其实不需要执行权限,因为bash会把它当作文本文件逐行读取解释。
还有一种情况:当前目录不在PATH环境变量里,直接敲test.sh会提示command not found,必须写成./test.sh,加了./告诉Shell去当前目录找这个文件。这些细节看起来琐碎,但新手在这里卡住的概率极高。
1.3 调试Shell脚本的“照妖镜”
我写脚本的时候一定会用到调试模式,这是排查问题最直接的方式:
bash复制bash -x test.sh
-x参数会把脚本每一条执行的命令展开打印到终端,前面带一个+号。这样你能清楚地看到某个变量到底被解析成了什么值,哪个条件判断没有走进去。比在脚本里到处加echo打印要高效得多,也方便你对照热词里大家常搜的“shell脚本常见坑”去逐条核对。
在脚本内部也可以临时开启调试区段:
bash复制set -x
# 需要调试的代码块
set +x
set -x开启,set +x关闭,非常灵活。另外还有set -e,它的作用是当脚本中任意一条命令执行返回非零状态码时,立即退出脚本。这个选项在自动化构建脚本里特别有用,防止某个步骤失败了还继续往下跑,带着一个残缺的产物去做后续操作,最后产生一堆“莫名其妙”的错误。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. Shell脚本最常用的一批“基础操作”
2.1 变量和花括号的学问
Shell变量定义很简单,注意等号两边不能有空格:
bash复制name="world"
echo "hello $name"
这里我强烈建议,凡是变量后面还要跟其他字符的,都要用${}包起来。比如:
bash复制version=1
echo "cmake-${version}.tar.gz"
如果不加大括号,Shell会把$version后面的.tar.gz当成变量名的一部分去解析,自然打印不出你想要的内容。
${}和$()是新手很容易搞混的两个重点,热词里也高频出现。一句话解释清楚:
${}:变量取值,取出某个变量的值。$():命令替换,先执行括号里的命令,然后把结果作为字符串返回。
示例:
bash复制current_time=$(date)
echo "$current_time"
因为Shell是动态解析的,所以$(date)会把date命令的输出当场当作一个字符串赋给current_time,这就是命令替换。老式写法是反引号:
bash复制current_time=`date`
反引号的问题是不好嵌套,而且跟单引号视觉上容易看错,所以现在统一推荐$()。同理,算术运算用$(( )):
bash复制a=3
b=5
echo $((a + b))
2.2 for循环、while循环和条件判断
for循环是写批量处理脚本时最高频的语法,热词里“shell脚本for循环”的搜索量一直很高。原因是它真的太常用了:批量重命名文件、批量编译目录、批量打包日志,全都是循环的活。
最基本的数字循环:
bash复制for i in 1 2 3 4 5; do
echo "number: $i"
done
更方便的是用{1..5}这种展开写法:
bash复制for i in {1..5}; do
echo "number: $i"
done
遍历当前目录下的所有.cpp文件:
bash复制for file in *.cpp; do
echo "processing $file"
done
C语言风格的for循环在bash里同样支持:
bash复制for ((i=0; i<5; i++)); do
echo $i
done
while循环用得多的场景是读取文件内容、等待某个进程退出、拉取用户输入:
bash复制count=0
while [ $count -lt 5 ]; do
echo "count: $count"
count=$((count + 1))
done
注意:while和if后面跟[ ]的时候,方括号内侧必须留一
