news 2026/10/23 5:22:49

OpenEuler Shell脚本起步:核心语法与调试排错实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
OpenEuler Shell脚本起步:核心语法与调试排错实战

刚接触 OpenEuler 的 Shell 编程时,很多人觉得脚本不过就是把几条 Linux 命令按顺序写进一个文件,然后执行一下,仅此而已。这个理解不算错,但真正动手之后你会发现,一个看起来再简单的起步脚本,里面也藏着解释器路径、变量展开、退出码、执行权限、相对路径这些一不小心就踩坑的细节。这篇是 OpenEuler Shell 编程系列的第 11 篇,我想认真拆一个能在 openEuler 上直接跑的起步脚本:从设计思路、核心语法到调试排错,把每一步为什么要这样做讲清楚。适合刚接触 shell 脚本入门的新手,也适合在 arm 架构 openEuler 服务器上做虚拟化运维的同学,把日常重复检查变成一键执行。

1. 为什么要把“起步脚本”当作一个正经项目来做

1.1 起步脚本不是玩具

很多人会把“简单脚本”和“不重要”画等号。我在实际运维里见过不少反过来教训的例子:一个只需要 20 行的健康检查脚本,因为没考虑退出码,被监控平台误判成“脚本执行失败”;另一个环境巡检脚本因为总在系统根目录下运行,导致日志文件写错位置,占满磁盘。越简单的脚本,运行频率往往越高,出问题后的影响范围也越广。

起步脚本的正确心态是“最小可用”而不是“随便写写”。它要完成一个明确的小目标,比如输出当前系统的基本状态、检查某个服务是否在运行、把一批文件的名称统一改掉。目标越小,越容易验证结果,也越容易在出错时定位问题。把这个最小闭环走通,后面再往里面加 for 循环、case 分支、函数定义,都是在同一个骨架上长肉。

所以这篇的定位不是教你写 Hello World 就完事,而是给你一个可以在 openEuler 上直接投入日常使用的起步脚本。它简单,但它是一个真正干活的东西。

1.2 在 openEuler 上学习 Shell 的独特优势

OpenEuler 是面向服务器场景的操作系统,装上它之后,你面对的就是真实的生产环境。在这里学 Shell 编程,不需要额外搭一套所谓的“实验环境”,你日常敲命令的地方就是最好的练手场。尤其是 arm 架构的 openEuler 服务器,很多人会用它搭配 libvirt-daemon-kvm 做虚拟化,这时候要批量查看虚拟机状态、检查服务存活、清理过期日志,手动一条一条敲命令会让人非常崩溃。起步脚本的价值在这个场景下会立刻显现出来。

另外,openEuler 默认的 Shell 环境是 Bash,这和绝大多数主流 Linux 发行版保持一致。你在 openEuler 上写的脚本,拿到其他 Linux 机器上基本可以直接跑,反过来也一样。这种兼容性降低了学习成本,也意味着你的脚本能力不会绑定在某一个特定系统上。

2. 起步脚本的设计思路:先想清楚再动笔

2.1 脚本要完成的最小目标

动手写脚本之前,先问自己一个问题:这个脚本到底要解决什么痛点?比如你每天登录服务器都要敲df -h看磁盘、敲free -m看内存、敲systemctl status看服务,这些操作是重复的,而且容易漏。那就把它们组合在一起,变成一个“环境巡检脚本”。它的目标很小,就是“一次输出当前系统的关键状态”。

设计时坚持三个原则:

  • 单一职责:一个起步脚本只做一类事情,不要把环境巡检、日志清理、服务重启全塞进去。
  • 退出码明确:脚本执行成功返回 0,遇到异常返回非 0,方便监控平台调用。
  • 输出可读:每条输出前面带明确标签,比如[DISK]、[MEM],别让人看半天猜不出哪行对应哪个指标。

这三个原则听起来简单,但很多老手写的脚本都做不到。起步阶段养成这个习惯,后面写大脚本才不会失控。

2.2 先避开这几个进阶特性

写第一个脚本的时候,我建议刻意避开几样东西:函数定义、getopts参数解析、关联数组、复杂的正则。不是这些特性不好,而是它们会把“脚本能跑”和“脚本为什么跑”这两件事混在一起。新手一旦引入过多特性,脚本报错时根本分不清是语法问题、变量问题还是逻辑问题。

我说的“避开”是暂时避开。当你把基础变量展开、条件判断、循环执行这些东西吃透之后,再回头用函数封装逻辑,会顺手很多。起步脚本的价值在于让你先建立对执行过程的感觉,而不是一步到位写出架构优雅的大脚本。先把路走通,再追求走好。

3. 核心语法拆解:起步脚本里的每一条都是知识点

3.1 第一行#!/bin/bash为什么是它

几乎每个 shell 脚本文件的第一行都是#!/bin/bash,这一行叫 shebang,它的作用是告诉系统用哪个解释器来执行这个文件。你可以试试把这行删掉,然后执行./check_env.sh,大概率会报错,因为系统不知道应该用什么程序去解释这个文件里的内容。

为什么用/bin/bash而不是/bin/sh?在 openEuler 上,/bin/sh通常指向 dash 或者一个兼容 POSIX 的精简 Shell,它的语法支持范围和 Bash 不完全一样。如果脚本里用了[[ ]]、source、数组这些 Bash 特性,用/bin/sh跑会直接报错,用/bin/bash跑就没问题。为了少踩莫名其妙的坑,统一写#!/bin/bash。这行后面如果加上调试选项也很有用,比如#!/bin/bash -x,可以在执行时把每条命令打印出来,便于观察脚本走向。平时先不加,调试时再用别的方式。

3.2 变量赋值与字符串拼接

Shell 入门第一个最容易翻车的点就是变量赋值。语法要求等号两边不能有空格,name=openEuler是赋值,name = openEuler会被解释成执行name命令并传递两个参数。这个区别新手很容易忽略,尤其在习惯了其他编程语言写法的情况下。

变量展开时,建议养成写${var}而不是$var的习惯。看下面这个例子:

#!/bin/bash LOG_DIR=/data/logs echo "${LOG_DIR}_backup" echo "$LOG_DIR_backup"

第一行会输出/data/logs_backup,第二行会因为$LOG_DIR_backup被误认为一个变量名而输出空行。这种边界问题在拼接路径、加后缀时非常常见。另一个绝对值得养成的习惯是变量展开时加双引号:echo "$var"。如果变量内容里带空格,不加引号会导致它被拆成多个参数,行为完全出乎意料。

3.3 if、for 与 test:控制结构怎么进脚本

条件判断里最坑的是空格。if [ -f "$file" ]中,[是命令,后面的-f、$file、]之间都必须有空格。写成if [-f "$file"]会直接报 "command not found"。这个写法不符合直觉,但它是 Shell 语法的一部分:[本质上是test命令的别名,命令和参数之间当然要空格。

[ ]和[[ ]]的区别也值得知道。[ ]是 POSIX 语法,里面不能用&&和||,必须写成-a和-o;[[ ]]是 Bash 扩展语法,支持&&、||、正则匹配,更直观。起步脚本里我推荐直接使用[[ ]],前提是 shebang 严格指向 Bash。例如:

#!/bin/bash FILE=/etc/openEuler-release if [[ -f "$FILE" ]]; then echo "release file exists" fi

for 循环是起步脚本里最实用的结构之一。比如你想批量重命名一堆文件,把.txt后缀改成.log:

#!/bin/bash for f in /tmp/test/*.txt; do mv "$f" "${f%.txt}.log" done

这里的${f%.txt}是参数展开,作用是去掉变量f末尾的.txt。这个技巧在处理批量文件时比sed更直接,也避免了子进程带来的复杂度。for 循环的常见写法还有for i in 1 2 3 4 5和for i in $(seq 1 5),做端口扫描或批量检查时可以用到。

3.4 shift、$@、$?:参数解析的入门三件套

起步脚本往往需要接收参数,比如./check_env.sh --path /var/log。这里的$1是第一个参数,$2是第二个,$@是所有参数。shift命令的作用是把参数列表往左移一位,原来的$2变成新的$1,方便逐个处理。

写一个简单的参数循环:

#!/bin/bash while [[ $# -gt 0 ]]; do case "$1" in --path) TARGET_PATH="$2" shift 2 ;; --verbose) VERBOSE=1 shift ;; *) echo "unknown option: $1" exit 1 ;; esac done

shift 2表示移走两个参数,这样--path /var/log作为一个整体被消费掉。exit 1是退出码,非 0 表示异常,外部监控程序拿到这个返回值就能判断脚本执行失败了。$?则用来读取上一条命令的退出码。在脚本里经常能看到这样的模式:

df -h > /dev/null 2>&1 if [[ $? -ne 0 ]]; then echo "df command failed" exit 1 fi

第一行的> /dev/null 2>&1是把标准输出和错误输出都丢弃,只保留退出码。这个组合在写脚本时出现的频率极高,也是排查问题时的重点。

4. 实操:从零写一个可在 openEuler 上直接跑的起步脚本

4.1 用 vim 创建脚本并设置可执行权限

我挑一个最常见的场景来实战:写一个环境巡检脚本,输出主机名、运行时长、磁盘使用率、内存使用率和关键服务状态。在 openEuler 终端里执行:

vim check_env.sh

把下面内容写进去:

#!/bin/bash echo "===== Basic Info =====" hostname uptime echo "===== Disk Usage =====" df -h | grep -v tmpfs echo "===== Memory Usage =====" free -m echo "===== Service Status =====" systemctl is-active sshd systemctl is-active libvirtd

这里每个部分都在回应最开始说的“最小目标”。df -h | grep -v tmpfs是为了过滤掉 tmpfs 这类内存文件系统的干扰,聚焦真实磁盘分区。systemctl is-active的输出很简单,要么active要么inactive,适合脚本直接判断。

下一步是赋予可执行权限:

chmod +x check_env.sh

关于执行权限有个细节值得记住:./check_env.sh需要读和执行权限,而bash check_env.sh只需要读权限。前者是文件作为程序被内核加载,后者是显式让 bash 去解释文件。日常测试时用bash check_env.sh更省事,但正式使用时我建议都用./check_env.sh,这样能尽早发现权限缺失的问题,避免部署到生产环境时突然失效。

4.2 运行输出解读与参数微调

直接运行脚本:

./check_env.sh

输出类似这样:

===== Basic Info ===== my-server 14:32:19 up 120 days, 3:25, 2 users, load average: 0.01, 0.03, 0.05 ===== Disk Usage ===== Filesystem Size Used Avail Use% Mounted on /dev/vda1 100G 62G 38G 63% / /dev/vda2 200G 80G 120G 40% /data ===== Memory Usage ===== total used free shared buff/cache available Mem: 7823 2048 3050 210 2725 5277 Swap: 2047 0 2047 ===== Service Status ===== active active

如果你在 arm 架构的 openEuler 服务器上跑,输出中的磁盘设备名可能是/dev/sda而不是/dev/vda,这取决于虚拟化底层。我这边是 qemu/kvm 环境,所以看到的是 virtio 设备。这不影响脚本逻辑,脚本本身和你用的是什么架构无关,这正是 Shell 脚本跨平台的价值。

如果想进一步加一个磁盘空间告警,可以取用率这一列:

#!/bin/bash THRESHOLD=80 USAGE=$(df -h / | awk 'NR==2 {print $5}' | tr -d '%') if [[ $USAGE -gt $THRESHOLD ]]; then echo "Warning: root disk usage is ${USAGE}%" else echo "Root disk usage is ${USAGE}%, OK" fi

$( )是命令替换,把df -h /的输出经过awk提取第五列,再用tr -d '%'去掉百分号,最终得到一个纯数字,然后和阈值比较。命令行里手动操作时你不会觉得这些命令有什么特别,但把它们组合在一起时,每一个管道和引号都可能让脚本行为偏离预期,所以要保持头脑清晰。

4.3 用 bash -x 进行最简单的调试

脚本跑通之后,我想强烈建议你体验一次调试模式。执行:

bash -x check_env.sh

-x选项会让 bash 在执行每条命令前把这条命令打印到标准错误,前面带一个+号。你会看到变量展开后的真实值,比如输出是+ df -h /而不是df -h /,这时就说明脚本已经从上到下按顺序执行到这里了。

当脚本出错时,bash -x的输出就是最有用的破案线索。比如[[ $USAGE -gt $THRESHOLD ]]这条,如果$USAGE是63,你看到的输出就是+ [[ 63 -gt 80 ]],一眼就能判断比较逻辑有没有问题。如果变量值是空的,那就不用费劲查下面的逻辑了,问题一定出在变量赋值那一步。

在脚本内部也可以局部开启调试,用set -x开启、set +x关闭,这样只针对某一段代码打印详细过程。这个方法比在整块脚本里加echo强得多,因为不会污染输出内容。

4.4 让起步脚本进入日常运维:crontab 定时执行

脚本写好了,手动执行没问题,接下来一个很自然的需求是让它定时跑。在 openEuler 上编辑 crontab:

crontab -e

添加一行,比如每隔 30 分钟执行一次:

*/30 * * * * /home/user/check_env.sh >> /var/log/check_env.log 2>&1

有两个地方特别容易踩坑。第一,crontab 里的命令要用绝对路径,因为定时任务环境的 PATH 很精简,相对路径和仅在交互式 Shell 里生效的变量都不可靠。第二,脚本的输出必须重定向到文件,否则 cron 会把输出通过邮件发给你,有时候邮件没配置,输出就悄悄丢失,让你以为脚本没跑。

如果想排除 cron 环境问题,可以在脚本开头加一行:

#!/bin/bash source /etc/profile

这样能加载系统环境变量,避免脚本里用到某些自定义变量时在 cron 环境下为空。

5. 常见问题与排查技巧实录

5.1 从 Windows 复制脚本后各种报错

这是一个出现频率极高的问题。你在 Windows 上用记事本或 IDE 写好脚本,然后上传到 openEuler 上执行,结果报错bad interpreter: /bin/bash^M: No such file or directory。这里的^M是 Windows 的 CRLF 换行符(\r\n)和 Linux 的 LF 换行符(\n)冲突造成的。Linux 把\r也当成了解释器路径的一部分,自然找不到这个“解释器”。

解决办法是在 openEuler 上执行:

sed -i 's/\r$//' check_env.sh

或者用dos2unix check_env.sh。从根上避免的方法是,在 Windows 端编辑时把换行符设置成 LF,几乎所有主流编辑器都支持这个设置。这个坑本身不大,但第一次遇到时非常容易让人困惑。

5.2 cd 相对路径引起的“找不到目录”

脚本里如果写了cd mydir,这个mydir是相对路径,它会相对于你执行脚本时所在的目录去解析。你在/home/user下执行./check_env.sh没问题,但放到 crontab 里从/下执行时,脚本开头那个cd mydir就找不到目录了。

正确的做法有两种。一种是用绝对路径:

cd /home/user/mydir

另一种是让脚本以自身所在目录为基准:

cd "$(dirname "$0")"

这里的$0是脚本自身的路径,dirname提取出目录部分。组合起来就是“无论脚本从哪里被调用,都先切换到脚本自己所在的目录”。这个写法在部署脚本、备份脚本里非常常见,也是我写脚本时的固定开头之一。

5.3 export 作用域与 source 的区别

export在 shell 编程里很容易被误解。它并不是把变量设为“全局变量”,而是把这个变量标记为“可以被当前 Shell 启动的子进程继承”。比如你在终端执行:

export MY_VAR=hello ./check_env.sh

在check_env.sh里是可以读到MY_VAR的;但如果不在终端里 export,脚本里就读不到。反过来也一样,脚本内部定义的普通变量,脚本结束后不会留在当前终端里。

还有一个和 export 紧密相关的知识点是source。执行source check_env.sh时,脚本是在当前 Shell 进程里被逐行解释执行的,它里面定义的变量会留在当前 Shell 里;执行./check_env.sh时,系统会启动一个新的子 Shell 来运行脚本,变量不会回流。所以如果你想在配置变更后马上让当前会话生效,就要用source,比如source ~/.bashrc。想隔离脚本里的变量污染,则用./script.sh。这个区别直接影响脚本的设计方式。

5.4 起步脚本常见坑位速查表

我把这个过程中最容易翻车的几个点整理成一张表,写脚本时可以对照着自查:

症状可能原因解决办法
报command not found变量赋值等号两边有空格,或[后没空格去掉空格,按[ 条件 ]格式写
变量输出为空变量名边界未加{}使用${var}
文件名带空格时处理失败变量展开未加双引号写成"$var"
bad interpreterWindows 换行符残留sed -i 's/\r$//' script.sh
crontab 里跑不出结果使用了相对路径全部改成绝对路径
set -e后脚本提前退出某条命令退出码非 0在可能失败的命令后加 `
管道符后判断失真管道中前一条命令失败但最后一条成功使用set -o pipefail

最后一条值得多解释两句。set -e的意思是“一旦有命令返回非 0,脚本立即退出”。但这个行为在管道里会打折,因为管道的退出码只看最后一个命令,比如df -h | grep tmpfs,如果df失败了,但grep没匹配到内容返回 1,脚本反而会退出;如果grep返回 0,脚本又继续跑。set -o pipefail可以让管道的退出码变成第一个失败命令的退出码,配合set -e用,脚本的行为才真正可预期。起步阶段可能用不上,但迟早会遇到。

写到这里,算是我个人在 openEuler 上折腾 shell 脚本的一点经验沉淀。启动脚本不一定要多高级,但一定要让每次执行都有明确的结果、明确的退出码,还要让后来的人看得懂输出。写完脚本后建议顺手跑一次shellcheck check_env.sh,这个静态检查工具能帮你找出很多隐藏问题,openEuler 的软件源里可以直接用dnf install shellcheck安装。我的体会是,把第一个简单的起步脚本认真打磨到“能交付给别人用”的程度,比随手写十个能跑的临时脚本,对能力的提升大得多。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/18 20:58:07

前缀和与哈希表:LeetCode 560题和为K的子数组最优解

LeetCode Hot 100 里的 560 题“和为 K 的子数组”,我前前后后刷了三遍,每次都有新的理解。这道题在面试和算法学习中出现的频率极高,看起来只是“数一数有多少个连续子数组的和等于 k”,可真正把它讲透的人不多。很多解法上来就丢…

作者头像 李华
网站建设 2026/10/18 23:08:23

提示词进阶实战:从拆解任务到驾驭多模态生成

不聊虚的,直接聊怎么用。这篇是提示词系列第四篇,前几篇把角色设定、上下文管理、输出约束这些基础过了一遍,这篇集中讲进阶场景:复杂任务拆解、编程场景提示词、多模态生成提示词,还有最常见的报错现场。核心就一句话…

作者头像 李华
网站建设 2026/10/18 23:08:24

Makefile 变量打印全攻略:调试构建系统必会的技巧与避坑指南

我先讲一个自己踩过的坑。有一回调试一块嵌入式板卡,交叉编译老是找不到头文件,我在网上翻了好几圈帖子,最后才发现其实是 Makefile 里的头文件路径变量被环境变量静默覆盖了。当时要是早点把变量打出来,大概十分钟就能定位问题&a…

作者头像 李华
网站建设 2026/10/18 23:08:27

从“smileNicky”聊起:技术博主如何靠踩坑实录写出高价值博客

1. 这个“smileNicky”到底是谁,为什么值得聊我最初注意到“smileNicky”这个名字,是在搜索技术博客的时候被反复带出来的。它不像一般网名那样只是随手起的代号,更像是一个长期活跃在开发者社区、持续输出内容的技术博主ID。如果你混过CSDN、…

作者头像 李华
网站建设 2026/10/18 23:08:24

Python五子棋双人对弈:毕业设计项目从下载到运行全指南

简介:这是一份面向Python初学者与计算机相关专业学生的五子棋游戏源码,可作为毕业设计选题或课程实践项目,下载后即可运行体验双人对弈。资源包共10个文件,以5个py源码文件为主,辅以4个pyc编译文件与1个txt说明文档&am…

作者头像 李华
网站建设 2026/10/18 23:08:26

Windows环境iOS真机自动化测试与日志CPU监控方案

1. 为什么 Windows 环境测 iOS 应用是个绕不开的话题1.1 iOS 工具链对 Mac 环境的“硬绑定”是怎么回事Windows 环境测 iOS 应用,放在几年前几乎是一个让人皱眉的组合。iOS 的官方开发工具链默认只跑在 MacOS 上,编译、签名、模拟器、在线调试全部绑在苹…

作者头像 李华