搞过一阵子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后面跟[ ]的时候,方括号内侧必须留一个空格。[ $count -lt 5 ]如果是[$count这样挨着写,Shell会找不到[这个命令的语法,直接报错。这里还有一个坑,如果$count是空值,[ $count -lt 5 ]会被展开成[ -lt 5 ],语法直接崩掉。稳妥的做法是给变量加双引号:[ "$count" -lt 5 ]。
条件判断的基本结构:
bash复制if [ -f "/etc/passwd" ]; then
echo "file exists"
else
echo "file not found"
fi
fi是if倒过来写的,用来结束这个条件块。常用的文件判断参数有:
bash复制[ -d "dir" ] # 是目录
[ -f "file" ] # 是文件
[ -e "path" ] # 路径存在
[ -z "$var" ] # 变量为空
[ -n "$var" ] # 变量非空
[ "$a" = "$b" ] # 字符串相等
[ "$a" -eq "$b" ] # 数字相等
2.3 shift命令与脚本参数处理
在写命令行工具脚本时,经常需要处理用户传来的参数。$0是脚本名,$1、$2依次是第1、2个参数,$#是参数总个数。
shift的作用是让所有参数整体左移一位,原来的$2变成$1,新的$0不变。最常见的用法是在while循环里配合case做参数解析:
bash复制while [ $# -gt 0 ]; do
case "$1" in
-b | --build)
BUILD=1
shift
;;
-c | --clean)
CLEAN=1
shift
;;
*)
echo "unknown option: $1"
exit 1
;;
esac
done
shift还可以带个数,比如shift 2表示一次左移两位。这种方式写出来的脚本,用起来就跟一个规范的命令行工具一样,./build.sh --clean --build,清晰而且可扩展。
2.4 Shell里常见的低端错误汇总
我在实际中帮别人排查脚本问题,发现大多数都是几个老毛病:
- 赋值等号周围有空格:
name = "value"会被Shell看成三段:执行命令name,参数是=和value,必然报command not found。 - 忘记给变量加双引号:如果值里带空格,不加引号的变量会被拆成多个单词,执行结果完全不是你想要的样子。
- 误用
[ ]还是[[ ]]:[[ ]]是bash扩展语法,不支持POSIX sh,但支持&&、||、正则匹配,写bash脚本时优先用它更省心。 - 在Windows上编辑Linux脚本:记事本会把换行符存成
\r\n,Linux读到\r就会报$'\r': command not found,用dos2unix转一下就行。
这些都是你如果用bash -x一调就能定位的问题,但新手往往会盯着语法本身看半天,完全没头绪。
3. 认识Cmake:一套跨平台构建规则
3.1 Cmake到底是什么,跟编译器有什么区别
先说结论:Cmake不是编译器,它是一个“构建系统生成器”。它读CMakeLists.txt文件,根据你写的配置,生成对应的本地构建文件。在Linux上默认生成Makefile,你只需要再执行make;如果你装了Ninja,它也可以生成build.ninja,执行ninja就能编译。在Windows上它可以生成Visual Studio的.sln工程文件,在macOS上可以生成Xcode工程。
有人可能会问,我直接用gcc编译不就行了?写个Makefile不也行?为什么非要Cmake?
因为Cmake解决了两个非常实际的问题:
- 跨平台统一:同样的CMakeLists.txt,在Linux、Windows、macOS几乎不用改,Cmake会根据当前平台自动选择合适的编译器和工具链。而手写Makefile,不同平台的路径、库文件、工具链都不一样,维护起来非常痛苦。
- 依赖管理:
find_package()能自动寻找系统里安装的第三方库,配置好头文件路径和链接库路径,省去到处改路径的苦力活。
所以Cmake已经是现代C/C++项目的实际“事实标准”。无论是从GitHub拉一个开源项目,还是用ESP-IDF开发嵌入式固件,默认的构建方式都是cmake。
3.2 CMakeLists.txt中最核心的几行
一个最基本的CMakeLists.txt长这样:
cmake复制cmake_minimum_required(VERSION 3.16)
project(hello_demo)
add_executable(hello main.cpp)
cmake_minimum_required:声明需要的最低Cmake版本号,版本太老就直接拒绝执行,避免语法兼容性问题。project:工程名,会作为变量提供给后续逻辑使用。add_executable:把main.cpp编译成可执行文件hello。
如果代码不止一个文件,需要指定多个源文件:
cmake复制add_executable(hello main.cpp utils.cpp logger.cpp)
如果文件太多,还可以用aux_source_directory收集一个目录下的所有源文件:
cmake复制aux_source_directory(src SRC_FILES)
add_executable(hello ${SRC_FILES})
给目标增加头文件搜索路径和链接库:
cmake复制target_include_directories(hello PRIVATE include)
target_link_libraries(hello PRIVATE pthread)
PRIVATE的意思是这个头文件和链接库只对hello这一个目标生效,不会传染给别的目标。如果是一个库文件,用PUBLIC则是要让下游使用者一起继承。这个权限控制理解起来有点像C++的访问控制,但语义要区分清楚。
3.3 一个最小项目的完整构建过程
假设你当前目录有这样的结构:
code复制hello_demo/
├── CMakeLists.txt
└── main.cpp
在项目根目录下执行:
bash复制mkdir build
cd build
cmake ..
make
./hello
这是Cmake项目最基本的“三连”。这里做的事情是:新建一个build目录存放所有构建产物,在build目录里执行cmake ..读取上一级目录的CMakeLists.txt并生成Makefile,再执行make完成实际编译。
为什么非要新建一个build目录,而不是直接在项目根目录下跑cmake?因为cmake会把一堆中间文件、缓存文件全部吐出来,直接污染源码目录。更麻烦的是,如果你想切换编译器,或者同时维护Debug和Release两套配置,在源码目录里会根本没法收拾。所以推荐使用“out-of-source build”,即构建目录和源码目录分开,这是在正规项目里最基本的好习惯。
4. Cmake安装与环境搭建:不同平台的踩坑记录
4.1 Linux下安装Cmake
在Debian/Ubuntu系统上,最简单的方式是:
bash复制sudo apt update
sudo apt install cmake
但系统源里的cmake版本通常比较旧。如果你从GitHub拉下来一个项目,它的cmake_minimum_required(VERSION 3.22),而你的apt源里还是3.16,就会直接报错提示找新版本。
解决办法有两种。第一种是添加官方Kitware的apt源,但国内访问可能不顺畅。第二种是直接去Cmake官网下载编译好的二进制包,解压就能用:
bash复制wget https://github.com/Kitware/CMake/releases/download/v3.27.6/cmake-3.27.6-linux-x86_64.tar.gz
tar -zxvf cmake-3.27.6-linux-x86_64.tar.gz
sudo mv cmake-3.27.6-linux-x86_64 /opt/cmake
然后把执行路径加到PATH里,在~/.bashrc末尾添加:
bash复制export PATH=/opt/cmake/bin:$PATH
执行source ~/.bashrc让配置生效。查看版本验证:
bash复制cmake --version
用二进制包的好处是版本完全可控,卸载也好办,删掉目录和PATH配置就行。
4.2 macOS下安装Cmake
macOS上用Homebrew是最省心的:
bash复制brew install cmake
有部分用户会发现cmake命令装好了,但开发工具链缺失导致项目配置失败,还需要安装Command Line Tools:
bash复制xcode-select --install
这也是苹果生态里比较常见的坑。Cmake在macOS上默认使用的编译器是clang,装完Xcode命令行工具之后才能正常工作。
4.3 Windows下安装Cmake:无法将“cmake”项识别为cmdlet
Windows下最典型的报错,就是热词里那句很长的提示:
code复制cmake : 无法将“cmake”项识别为 cmdlet、函数、脚本文件或可运行程序的名称。
这本质上就是cmake.exe不在系统的PATH环境变量里,PowerShell找不到这个命令。解决办法很简单:
- 去Cmake官网下载Windows安装包,通常选择
Windows x86_64 Installer。 - 安装过程中一定要勾选**“Add CMake to the system PATH for all users”**,这是新手最容易忽略的一步。
- 安装完成后重新打开PowerShell或终端,执行
cmake --version验证。
如果当时安装的时候没勾选PATH,也可以手动添加。在系统环境变量里找到Path,新增一条指向C:\Program Files\CMake\bin的路径,然后重启终端。这一步操作完,就不会再报“无法将cmake项识别为cmdlet”了。
Windows上使用Cmake还有个问题:默认生成的工程是Visual Studio的.sln,里面会区分Debug/Release、Win32/x64这些配置。如果你不喜欢VS也能跑,可以指定生成器为MinGW Makefiles或Ninja,但前提是你装了对应工具链。我在Windows上尝试过的命令:
cmd复制cmake -G "MinGW Makefiles" ..
mingw32-make
用MinGW的前提是你安装并配置好了MinGW-w64工具链,且mingw32-make在执行路径里。相比Linux下直接make,Windows下确实要留意生成器选择,这是个常见易错点。
4.4 Cmake是Wind10 64位,有版本要求吗
关于热词里“cmake wind10 64位”这个问题,Cmake对Windows 10 64位的支持非常成熟,下载官方64位安装包就行,无需安装额外依赖。唯一要注意的是安装路径不要带中文或空格,比如C:\Program Files\CMake自带的空格其实也能正常工作,但在自己的脚本里处理路径时最好用引号包起来。
如果你正在用脚本自动调用cmake,Windows下路径问题最保险的方式,是最好在CMakeLists.txt或脚本内部用相对路径,避免硬编码带空格的绝对路径。
5. 实战:用Shell脚本把Cmake构建一条龙自动化
5.1 一个build.sh脚本的基本结构
在实际项目中,CMake的构建命令慢慢就会变多:先要clean、然后configure、再build,可能还要跑测试和执行安装。手动一条一条敲,重复而且容易漏。这时候写一个build.sh,把所有环节串起来,效果立竿见影。
下面这个脚本我在很多小项目里反复使用,结构基本不变:
bash复制#!/bin/bash
set -e
BUILD_DIR="build"
BUILD_TYPE="Debug"
mkdir -p "$BUILD_DIR"
cd "$BUILD_DIR"
cmake -DCMAKE_BUILD_TYPE=$BUILD_TYPE ..
make -j$(nproc)
set -e很重要,如果某一步出错就立刻停止脚本,避免后续步骤操作残缺的目录。-j$(nproc)是用CPU核心数来并行编译,明显提升速度。
5.2 加上清理功能与参数选择
再升级一下,让它支持--clean和--release参数:
bash复制#!/bin/bash
set -e
BUILD_DIR="build"
BUILD_TYPE="Debug"
if [ "$1" = "--clean" ]; then
rm -rf "$BUILD_DIR"
fi
if [ "$1" = "--release" ]; then
BUILD_TYPE="Release"
fi
mkdir -p "$BUILD_DIR"
cd "$BUILD_DIR"
cmake -DCMAKE_BUILD_TYPE=$BUILD_TYPE ..
make -j$(nproc)
echo "build finished. output in $BUILD_DIR"
实际执行:
bash复制./build.sh --clean
这样的脚本就是完整的“一键构建”。工程越大,你会越能体会到这个自动化脚本的价值。如果不小心手滑删了源码目录,那又是另一个让人印象深刻的教训。
5.3 多平台脚本的注意事项
跨平台是Cmake的强项,但写Shell脚本时要注意,Windows默认不是用bash的。为了在Windows下也能一键构建,一种做法是用CMake自身的能力来统一,但有时候用户还是更倾向于双击.bat文件,或者干脆在CMakeLists里把CMAKE_BUILD_TYPE和各种选项配置好,用户只需要打开终端敲两三条命令就行。
另一种做法:如果团队用的是Git Bash,那Windows下执行build.sh也可以,但要注意路径分隔符。Cmake生成的是Windows本地路径,C:\这样反斜杠在bash中容易出问题,最好在CMakeLists中把路径分隔问题统一交给file(TO_CMAKE_PATH)这类函数处理,或者干脆习惯在Git Bash中用/c/Projects/xxx这样的路径表达形式。
5.4 如何把Keil工程变成CMake工程
这也是一个很现实的诉求,热词里出现了“如何将keil工程变成cmake”。Keil工程文件是.uvprojx,本质上是XML存储的一套编译配置,包括芯片型号、宏定义、头文件路径、源文件列表。要把Keil工程迁移到Cmake,核心思路是:先理清Keil工程里的这些信息,然后在CMakeLists.txt里逐一还原。
比如STM32F407这颗芯片,用arm-none-eabi工具链,CMakeLists.txt大致长这样:
cmake复制cmake_minimum_required(VERSION 3.20)
project(stm32_demo)
set(CMAKE_SYSTEM_NAME Generic)
set(CMAKE_SYSTEM_PROCESSOR arm)
set(TOOLCHAIN_PREFIX arm-none-eabi-)
set(CMAKE_C_COMPILER ${TOOLCHAIN_PREFIX}gcc)
set(CMAKE_CXX_COMPILER ${TOOLCHAIN_PREFIX}g++)
add_executable(firmware
src/main.c
src/stm32f4xx_hal_msp.c
src/system_stm32f4xx.c
)
target_include_directories(firmware PRIVATE
Inc
Drivers/STM32F4xx_HAL_Driver/Inc
)
target_compile_definitions(firmware PRIVATE
STM32F407xx
USE_HAL_DRIVER
)
target_link_options(firmware PRIVATE
-T stm32f407vetx_flash.ld
--specs=nano.specs
)
迁移过程中容易出问题的地方是链接脚本文件(.ld文件),必须保证路径正确,芯片内部Flash和RAM大小要匹配。另外一个常见的坑是Keil里有“C/C++”选项卡下的“One ELF Section per Function”,对应到GCC工具链就是-ffunction-sections -fdata-sections加上链接时的--gc-sections,如果忘了加,固件体积就会膨胀很多。迁移后用arm-none-eabi-objcopy -O binary firmware firmware.bin生成bin文件烧录就行。
6. 从基础到进阶:Cmake在真实项目里的打开方式
6.1 find_package和第三方库
真实项目里几乎都会依赖第三方库,不可能把每一个库的源码都手动编译一遍。Cmake提供了find_package()来解决这个问题。
比如你系统里安装了OpenCV,那么在CMakeLists.txt里这样写:
cmake复制find_package(OpenCV REQUIRED)
target_link_libraries(hello PRIVATE ${OpenCV_LIBS})
target_include_directories(hello PRIVATE ${OpenCV_INCLUDE_DIRS})
find_package会在系统路径和Cmake自身的模块目录里寻找FindOpenCV.cmake这种查找脚本,找到后就填充OpenCV_LIBS和OpenCV_INCLUDE_DIRS这两个变量。如果系统里没有安装某个库,REQUIRED会让cmake直接报错退出,而不是继续配置最后在编译期才报一堆找不到头文件的错。
这类“库没装齐”的问题在Linux下很常见,通常先用包管理器安装对应的-dev或-devel包。比如Ubuntu下:
bash复制sudo apt install libssl-dev
装完之后重新执行cmake,一般就能顺利通过。
6.2 嵌入式项目里的Cmake:与ESP-IDF、STM32结合
Cmake在嵌入式开发里的作用越来越明显。以ESP-IDF为例,它本身就是用cmake构建的,新建一个工程后,你需要在CMakeLists.txt里加入:
cmake复制include($ENV{IDF_PATH}/tools/cmake/project.cmake)
project(my_esp_app)
这样ESP-IDF记在IDF_PATH环境变量指向的路径,工程构建就能自动接入整个ESP-IDF的编译体系。官方文档里的示例都是这样的结构,环境变量IDF_PATH如果没有设置,这里就会直接报错,所以这也是热词里出现include($env{idf_path}/tools/cmake/project.cmake)的原因。
对于STM32项目,上一篇提到的Keil工程转Cmake只是第一步。更强大的地方是,你可以写一个Cmake函数,把各个模块抽象成可重用的组件,比如:
cmake复制add_library(driver_uart src/uart.c)
target_include_directories(driver_uart PUBLIC inc)
这样上层应用只需要target_link_libraries(app PRIVATE driver_uart),整个项目的模块边界非常清晰。
还有一个热词是“用letter shell打造stm32交互式命令行”。letter shell是一个嵌入式终端的开源库,移植的时候直接把源码加进CMakeLists.txt,然后在main函数里初始化,就能在串口上获得一个类似Linux终端的交互环境。用cmake来组织这种多文件夹、多模块的嵌入式工程,比在IDE里手动拖拽文件要可靠得多,也方便做持续集成。
6.3 其他与Shell相关的实用操作
热词里还有一些实用的操作,一并说下:
adb shell dumpsys battery:排查Android设备功耗问题时的常用命令。
bash复制adb shell dumpsys battery set usb 0
adb shell dumpsys battery set ac 0
第一行是把USB供电模拟为断开状态,让设备进入真实电池放电的工况。这样测出的电流曲线才反映真实功耗,而不是一直被充电状态干扰。测完记得恢复正常:
bash复制adb shell dumpsys battery reset
ssh免密登录执行shell:自动化部署场景下的刚需操作,常见于脚本里需要远程执行命令。
第一步用ssh-keygen -t rsa生成密钥对,然后使用ssh-copy-id user@host把你的公钥加到目标机器的~/.ssh/authorized_keys里,之后就可以直接执行:
bash复制ssh user@host "ls /opt && df -h"
不用再输密码。注意,这个主要是为了脚本自动化方便,不要随意把私钥外传。
华为交换机 shell request failed on channel 0:这个一般在用ssh登录华为交换机远程执行命令时出现,通常是连接复用或者SSH会话超时导致的。解决思路比较老套,断开重连,或者检查一下SSH配置里的ServerAliveInterval,让连接保持活性。这个在自动化网络设备运维时会遇到,如果不做网络运维,作为知识面了解一下就行。
blur my shell:一个美化Linux终端背景的工具,配置起来其实也很简单。它的原理是给GNOME Terminal的背景加一层实时模糊效果。我对终端美化的态度,一直是“工作环境舒服就好,别在美化上花太多时间”,但它能把终端真正变好看,视觉党可以试试。
6.4 用Shell脚本批量交流与自动化部署
把Shell和Cmake组合起来,能达到真正“一条命令从源码到部署”的效果。举个例子,我经常在服务器上做这样的自动化发布流程:
bash复制#!/bin/bash
set -e
PROJECT_DIR="/opt/project"
REMOTE_HOST="build-server"
scp -r ./src "$REMOTE_HOST:$PROJECT_DIR/src"
scp CMakeLists.txt "$REMOTE_HOST:$PROJECT_DIR"
ssh "$REMOTE_HOST" "cd $PROJECT_DIR && mkdir -p build && cd build && cmake .. && make && ctest"
这个脚本负责把代码上传到远端构建服务器,然后在远端执行cmake配置、编译和测试。出错就退出,成功就继续,整个流程完全自动化。这种模式跟CI的原理一致,只是本地写脚本更灵活、更透明,特别适合没有CI系统的场景。
7. 常见问题与排查技巧实录
把这段时间热词里见到的问题集中整理一个速查表,方便对照排查:
| 问题现象 | 常见原因 | 解决方法 |
|---|---|---|
| cmake : 无法将“cmake”项识别为 cmdlet | Windows下cmake未加入PATH | 安装时勾选Add to PATH,或手动添加环境变量 |
| /bin/sh: wq: command not found | 操作vim时在正常模式下输入了wq,或脚本里执行了wq命令 |
先按Esc退出编辑模式,再输入:wq保存退出 |
| [no write since last change] | 文件只读或修改后未正确保存就退出 | 检查文件权限,用:wq!强制保存,或确认文件是否被占用 |
| efi shell cannot find required map name | 在UEFI Shell下访问文件系统时映射名错误 | 输入map -r刷新映射列表,用fs0:访问分区 |
| include($ENV{IDF_PATH}/tools/cmake/project.cmake)报错 | 环境变量IDF_PATH未设置或路径错误 | export IDF_PATH=/path/to/esp-idf后重新cmake |
| shell request failed on channel 0 | SSH连接失效或服务端超时 | 重连SSH,或检查SSH KeepAlive配置 |
$'\r': command not found |
脚本文件含Windows换行符 | 使用dos2unix转换,或用VS Code切换行尾为LF |
再单独说一说那个最经典的vim退出问题。很多新手在终端里打开vim,想退出的时候直接敲了wq,结果Shell把这个当成了一条命令,报了/bin/sh: wq: command not found。正确的操作是在vim的普通模式下按:进入命令行模式,然后输入wq回车。如果文件是只读的,需要用:wq!强制保存退出。这里的关键是要先按Esc,确保自己处于普通模式而不是插入模式,然后输冒号。
排查这类问题的通用SOP,我自己一般是这样:
- 先把错误信息完整读一遍,不要跳着看。很多时候报错已经指明了文件和行号。
- 用
bash -x或者cmake --trace把执行过程展开,看看实际解析出来的命令和参数是什么。 - 如果是依赖问题,先确认版本:
cmake --version、gcc --version、make --version。 - 如果是Cmake配置报错,先清空build目录再重新cmake,避免缓存影响。
- 确认当前目录和路径:
pwd、ls -la,排除路径错误。
最后再分享一个我在实际项目里的体会。Shell和Cmake学起来都不难,难的是把它们组合起来解决真实问题。写脚本的时候永远记得加错误处理,set -e不是可有可无的,它能让你的自动化流程在出错的第一时间停下来。用Cmake的时候永远记得用out-of-source build方式,别让你的源码目录变得乱糟糟。这些习惯一旦养成,后面做再大的工程也不会手忙脚乱。
