1. 系统版本信息到底藏在哪先搞清楚要抓什么很多人第一次接触这个需求场景都差不多写了一个自动化脚本想让它根据不同的系统发行版本走不同的分支逻辑比如在某个版本上装特定版本的依赖包或者在另一个版本上调整某个配置文件的路径。结果一上来就卡在第一步——怎么让脚本自己知道当前跑在什么系统上。Linux 系统里跟发行版本相关的信息散落在好几个地方不是集中在一个文件里的。这一点跟 Windows 很不一样Windows 基本就是注册表里一个键值搞定Linux 这边得看你的发行版属于哪个家族。最核心的几个信息源是这样的/etc/os-release这是目前最主流、最推荐的位置。systemd 时代之后几乎所有主流发行版都会提供这个文件里面是标准的KEYvalue格式解析起来非常友好。/etc/issue这个文件历史很悠久通常是给登录提示用的内容格式五花八门有的发行版写得很规整有的就一行字符串解析起来比较头疼。/etc/lsb-releaseUbuntu 系和部分 Debian 系会有也是KEYvalue格式但不如os-release通用。/etc/redhat-release、/etc/centos-release、/etc/debian_version这些是特定发行版家族自己的文件名字就带着发行版特征。uname -r这个拿到的是内核版本注意内核版本和发行版本是两码事。很多人会把这两个搞混比如内核是 5.15但发行版可能是 Ubuntu 22.04也可能是别的两者没有一一对应关系。我见过不少脚本直接去解析/etc/issue结果换一个发行版就崩了因为那个文件的格式实在太随意。所以如果你要写一个相对通用的东西优先盯住/etc/os-release拿不到再降级去尝试其他文件。那为什么标题里强调的是grep配合正则表达式因为在实际运维和脚本编写中grep是最随手可得的文本处理工具几乎任何 Linux 环境都有不需要额外装 Python 或者别的解释器。用grep加正则可以在一行命令里把想要的那个字段抠出来非常适合塞进 shell 脚本里做判断。这个内容适合谁看我觉得三类人最需要一是刚接触 Linux 运维、开始写自动化脚本的朋友二是需要做跨发行版兼容部署的开发者三是平时用命令行比较多、想把手动操作沉淀成脚本的效率党。哪怕你正则表达式写得不多跟着下面的思路走一遍也能把这块吃透。2. 为什么偏偏选 grep 加正则方案选型的背后逻辑2.1 不用其他工具的理由拿到系统版本这个需求能用的工具其实不少。Python 有platform模块一条platform.linux_distribution()就完事了虽然这个函数后来被废弃了。lsb_release -a命令也能直接输出看起来更省事。那为什么还要绕一圈用grep加正则第一个原因是依赖问题。lsb_release这个命令并不是所有系统都预装的尤其是一些精简的容器镜像里压根就没有这个包。你脚本里写了lsb_release跑到一个没装的环境里直接报 command not found整个流程就断了。而grep是 coreutils 或者基础工具集里带的基本上有 shell 的地方就有它。第二个原因是可控性。lsb_release的输出格式是它自己定的你只能被动接受。而用grep去解析os-release你能精确控制要抓哪个字段、用什么模式匹配、匹配不到时怎么处理。这种可控性在写健壮脚本的时候特别重要。第三个原因是学习成本和通用性。正则表达式这套东西你学会了不光能用来抓版本号日志分析、配置提取、文本清洗到处都能用。花一次时间掌握后面受益的地方很多。而lsb_release这种专用命令会一个就只会一个。2.2 正则在这里扮演什么角色grep本身是干“按行筛选”这件事的它把包含匹配模式的行挑出来。但光挑出行还不够因为os-release里一行是VERSION_ID22.04这种形式你要的是等号右边那个值还得把引号和键名去掉。这时候正则的分组能力就派上用场了。举个直观的例子。文件里有一行VERSION_ID22.04我想拿到22.04。思路是匹配VERSION_ID开头然后捕获中间的内容最后匹配结尾的引号。用grep -oP配合 lookbehind 和 lookahead 就能做到只输出中间那段。或者用sed的替换功能把不要的部分替换成空。标题里说的是grep配合正则那我们就以grep为主sed作为补充手段。这里有个关键点要说明grep默认用的是基本正则表达式BRE-E开启扩展正则ERE-P开启 Perl 兼容正则PCRE。PCRE 支持 lookbehind、lookahead、\d、\K这些好用的语法做字段提取会方便很多。但-P不是所有grep实现都支持GNU grep 支持BusyBox 的 grep 就不一定支持。所以如果你的脚本要跑在很精简的环境里得考虑这个兼容性。2.3 一个容易被忽略的坑引号的有无不同发行版写os-release的时候值的引号处理并不完全一致。大部分是带双引号的比如VERSION_ID22.04但也有不带引号的写法。如果你的正则写死了必须匹配引号遇到不带引号的就抓不到。稳妥的做法是把引号做成可选的用?这种量词来处理。这个细节我在实际写脚本的时候踩过当时在一个定制化的镜像里怎么都抓不到版本号排查了半天才发现那个文件的字段没加引号。所以后面我写正则都会把引号部分做成可选的兼容性一下子就好了。3. 核心文件逐个拆解与正则写法3.1 os-release 的字段结构先看看/etc/os-release典型长什么样。以某个常见发行版为例内容大致是NAMEUbuntu VERSION22.04.3 LTS (Jammy Jellyfish) IDubuntu ID_LIKEdebian PRETTY_NAMEUbuntu 22.04.3 LTS VERSION_ID22.04 HOME_URLhttps://www.ubuntu.com/ SUPPORT_URLhttps://help.ubuntu.com/ BUG_REPORT_URLhttps://bugs.launchpad.net/ubuntu/ PRIVACY_POLICY_URLhttps://www.ubuntu.com/legal/terms-and-policies/privacy-policy VERSION_CODENAMEjammy UBUNTU_CODENAMEjammy这里面几个字段值得关注ID发行版的标识符小写比如ubuntu、debian、centos、fedora、alpine。这个字段最适合用来做脚本里的分支判断因为它稳定、简短、没有空格。VERSION_ID版本号通常是数字加点比如22.04、11、9。这个也适合做判断但要注意有的版本号带引号有的不带。PRETTY_NAME给人看的完整名称带空格和括号适合展示不适合做逻辑判断。ID_LIKE表示这个发行版跟哪个家族相似比如 Ubuntu 的ID_LIKEdebian。做兼容判断的时候有用。所以如果你的目的是“判断当前是什么系统然后走不同逻辑”优先用ID和VERSION_ID这两个字段别去解析PRETTY_NAME那个太不稳定。3.2 用 grep 提取 ID 字段最直接的写法grep -oP ^ID\K.* /etc/os-release拆解一下这个命令。-o表示只输出匹配到的部分而不是整行。-P启用 PCRE。^ID匹配行首的ID。\K是 PCRE 的一个特性意思是“把前面匹配到的内容从最终结果里丢掉”所以它前面的^ID不会出现在输出里。.*匹配剩下的所有字符。如果那个文件里IDubuntu不带引号这条命令输出就是ubuntu。如果带引号IDubuntu输出就是ubuntu引号还在。所以还得处理引号。处理引号有两种思路。一种是在正则里就把引号排除掉grep -oP ^ID?\K[^]* /etc/os-release这里?表示引号可有可无[^]*表示匹配任意多个非引号字符。这样不管有没有引号输出的都是纯值。另一种思路是用sed后处理grep ^ID /etc/os-release | sed s/^ID//; s///g先 grep 出那一行然后用 sed 把ID和所有引号都删掉。这种写法不依赖-P兼容性更好但多了一次管道。3.3 提取 VERSION_ID 的注意事项VERSION_ID的提取跟ID类似grep -oP ^VERSION_ID?\K[^]* /etc/os-release但这里有个坑有些发行版的os-release里可能没有VERSION_ID这个字段或者字段名大小写有差异。虽然标准规定是大写下划线但实际环境中不能完全假设它一定存在。所以脚本里拿到结果后要判断是否为空VERSION_ID$(grep -oP ^VERSION_ID?\K[^]* /etc/os-release) if [ -z $VERSION_ID ]; then echo 未能获取版本号 fi另外VERSION_ID的值可能是22.04这种带点的也可能是9这种纯数字。如果你要拿它做数值比较得注意带点的情况不能直接当整数比。比如[ $VERSION_ID -ge 20 ]在VERSION_ID22.04的时候会报错因为 shell 的-ge只认整数。这种时候要么用bc做浮点比较要么只取主版本号。3.4 降级方案其他文件的解析万一/etc/os-release不存在怎么办虽然现在很少见但老系统或者极度精简的环境里还是有可能。这时候可以按顺序尝试其他文件。/etc/lsb-release的格式跟os-release很像也是KEYvaluegrep -oP ^DISTRIB_ID?\K[^]* /etc/lsb-release grep -oP ^DISTRIB_RELEASE?\K[^]* /etc/lsb-release/etc/redhat-release是一整行文本比如CentOS Linux release 7.9.2009 (Core)。这种就得用正则从字符串里抠版本号grep -oP release \K[0-9.] /etc/redhat-release/etc/debian_version更简单通常整个文件就一个版本号直接cat就行但为了统一风格也可以用 grepgrep -oP [0-9.] /etc/debian_version把这些降级方案串起来就形成了一个比较健壮的获取逻辑。下面这个表格把各个文件的特点和适用场景整理了一下文件路径格式通用性推荐优先级/etc/os-releaseKEYvalue极高最高/etc/lsb-releaseKEYvalue中Ubuntu/Debian 系次高/etc/redhat-release单行文本低RedHat 系降级/etc/debian_version单行版本号低Debian 系降级/etc/issue自由文本中但格式乱不推荐4. 完整实操从一行命令到可复用脚本4.1 先手动跑一遍验证思路在动手写脚本之前我习惯先在命令行里把每一步都跑通确认输出符合预期再固化到脚本里。这样排查问题的时候心里有数。第一步确认文件存在ls -l /etc/os-release第二步看看文件内容长什么样cat /etc/os-release第三步试着提取 IDgrep -oP ^ID?\K[^]* /etc/os-release如果输出是干净的发行版标识比如ubuntu或者centos说明正则没问题。如果输出带了引号或者别的字符就调整正则。第四步提取版本号grep -oP ^VERSION_ID?\K[^]* /etc/os-release第五步把两个结果拼起来看echo $(grep -oP ^ID?\K[^]* /etc/os-release) $(grep -oP ^VERSION_ID?\K[^]* /etc/os-release)这一步跑通核心逻辑就验证完了。4.2 封装成函数在脚本里反复写这两条 grep 太啰嗦封装成函数更清爽get_os_id() { grep -oP ^ID?\K[^]* /etc/os-release 2/dev/null } get_os_version() { grep -oP ^VERSION_ID?\K[^]* /etc/os-release 2/dev/null }2/dev/null是为了在文件不存在时不输出错误信息让函数安静地返回空值由调用方去判断。然后主逻辑里就可以这样用OS_ID$(get_os_id) OS_VER$(get_os_version) if [ -z $OS_ID ]; then echo 无法识别系统发行版 exit 1 fi echo 当前系统: $OS_ID $OS_VER case $OS_ID in ubuntu|debian) echo 走 Debian 系逻辑 ;; centos|rhel|fedora) echo 走 RedHat 系逻辑 ;; alpine) echo 走 Alpine 逻辑 ;; *) echo 未知发行版走通用逻辑 ;; esac4.3 加上降级处理的完整版本把前面说的降级方案整合进去形成一个更健壮的版本detect_os() { if [ -f /etc/os-release ]; then OS_ID$(grep -oP ^ID?\K[^]* /etc/os-release 2/dev/null) OS_VER$(grep -oP ^VERSION_ID?\K[^]* /etc/os-release 2/dev/null) fi if [ -z $OS_ID ] [ -f /etc/lsb-release ]; then OS_ID$(grep -oP ^DISTRIB_ID?\K[^]* /etc/lsb-release 2/dev/null | tr A-Z a-z) OS_VER$(grep -oP ^DISTRIB_RELEASE?\K[^]* /etc/lsb-release 2/dev/null) fi if [ -z $OS_ID ] [ -f /etc/redhat-release ]; then OS_ID$(grep -oiP ^\K\w /etc/redhat-release 2/dev/null | tr A-Z a-z) OS_VER$(grep -oP release \K[0-9.] /etc/redhat-release 2/dev/null) fi if [ -z $OS_ID ] [ -f /etc/debian_version ]; then OS_IDdebian OS_VER$(grep -oP [0-9.] /etc/debian_version 2/dev/null) fi echo ${OS_ID:-unknown} ${OS_VER:-unknown} }这个函数按优先级依次尝试哪个先拿到结果就用哪个。tr A-Z a-z是为了统一大小写因为有的文件里写的是Ubuntu有的写ubuntu统一成小写方便后面比较。4.4 参数选择与性能考量有人可能会问grep -oP里的-P会不会有性能问题。实际上对于这种小文件性能完全不是瓶颈os-release通常就十几行几毫秒的事。真正需要注意的是兼容性前面提过 BusyBox 的 grep 可能不支持-P。如果你的脚本要跑在 Alpine 这种用 BusyBox 的环境里就得换成不依赖-P的写法。比如用sedsed -n s/^ID\?\([^]*\)\?/\1/p /etc/os-release或者用awkawk -F /^ID/{gsub(//,,$2); print $2} /etc/os-releaseawk的兼容性也很好几乎所有环境都有。所以如果你追求最大兼容性awk方案其实比grep -P更稳。但标题说的是 grep 配合正则所以主体还是用 grep把 awk 作为备选提一下。5. 常见问题与排查技巧实录5.1 抓出来的值带了引号怎么办这是最常见的问题。原因就是正则里没有处理引号。解决办法前面说过用?让引号可选用[^]*匹配非引号内容。如果你已经抓出来了带引号的值也可以事后清理OS_ID$(echo $OS_ID | tr -d )tr -d 把双引号删掉。这个方法简单粗暴适合作为兜底。5.2 为什么在容器里抓不到容器环境是个重灾区。很多精简镜像为了减小体积会把/etc/os-release删掉或者替换成极简版本。我遇到过某个镜像里/etc/os-release只有两行ID和VERSION_ID都没有。这种情况下任何基于这个文件的解析都会失败。应对办法就是前面说的降级链多准备几个备选文件。如果所有文件都没有那就只能靠uname拿内核信息或者干脆在构建镜像的时候自己写入一个标识文件。这个坑我在做多架构镜像构建的时候踩过后来养成了习惯凡是依赖系统信息的脚本都要考虑“信息不存在”的分支。5.3 版本号比较时的类型陷阱前面提过VERSION_ID可能是22.04这种带点的。如果你写if [ $OS_VER -ge 20 ]; then在OS_VER22.04的时候会报integer expression expected。因为 shell 的数值比较只认整数。解决办法有几个。一是只取主版本号MAJOR_VER${OS_VER%%.*} if [ $MAJOR_VER -ge 20 ]; then${OS_VER%%.*}是参数扩展表示从右边删掉第一个点及其后面的内容22.04就变成22。二是用bc做浮点比较if [ $(echo $OS_VER 20 | bc) -eq 1 ]; then三是用sort -V做版本排序比较这个在需要判断“大于等于某版本”的时候很好用if [ $(printf %s\n 20 $OS_VER | sort -V | head -n1) 20 ]; then这几种方法各有适用场景简单判断用第一种精确比较用第二种批量版本排序用第三种。5.4 常见问题速查表问题现象可能原因解决办法输出为空文件不存在或字段缺失检查文件启用降级链输出带引号正则未处理引号用?和[^]*报 integer expression expected版本号带小数点取主版本号或用 bc容器里失败精简镜像删了文件降级或自写标识grep 报不支持 -PBusyBox 环境改用 sed 或 awk大小写不一致不同发行版写法不同用 tr 统一小写5.5 几个实操心得第一个心得永远给变量加默认值。${OS_ID:-unknown}这种写法能避免变量为空时脚本行为异常。我见过太多脚本因为一个空变量导致后面逻辑全乱的情况。第二个心得调试时把中间结果打出来。写复杂正则的时候别一口气写完就指望它一次跑对。先 grep 出整行看看再逐步加-o、加\K、加字符类每一步都确认输出符合预期。这样出问题的时候能立刻定位是哪一步的锅。第三个心得正则里的\K不是万能的。它虽然好用但属于 PCRE 特性换到不支持的环境就废了。如果脚本要跨环境尽量用sed的分组替换或者awk的字段分割这些更通用。第四个心得注意行尾的空白字符。有些文件的字段值后面可能带空格或者\r尤其是从 Windows 传过来的文件。抓出来的值如果带了这些看不见的字符比较的时候就会莫名其妙不相等。可以用tr -d \r清理一下。6. 正则表达式的进阶用法与扩展场景6.1 用分组捕获多个字段有时候你想一次抓多个字段比如同时要ID和VERSION_ID。可以用grep -oP配合多个模式或者用sed的分组。用 grep 的话可以这样grep -oP ^(ID|VERSION_ID)?\K[^]* /etc/os-release这会输出两行第一行是 ID 的值第二行是 VERSION_ID 的值。但顺序取决于文件里的顺序不一定可靠。更稳妥的是分开抓或者用awk一次处理awk -F /^(ID|VERSION_ID)/{gsub(//,,$2); print $1$2} /etc/os-release这样输出的是IDubuntu和VERSION_ID22.04键值对清晰。6.2 处理 PRETTY_NAME 里的复杂内容PRETTY_NAME通常带空格和括号比如Ubuntu 22.04.3 LTS。如果你想抓它正则要允许空格grep -oP ^PRETTY_NAME?\K[^]* /etc/os-release这个跟抓 ID 的写法一样因为[^]*本身就允许空格。但要注意如果值里本身包含引号虽然很少见这个正则会提前截断。不过os-release标准里值是不含引号的所以问题不大。6.3 从 uname 输出里提取信息虽然uname给的是内核信息不是发行版本但有时候也需要。比如uname -r输出5.15.0-91-generic。如果你想提取主版本号uname -r | grep -oP ^\K[0-9]\.[0-9]输出5.15。这个在做内核相关判断的时候有用比如某些功能需要特定内核版本以上。6.4 把逻辑扩展到其他配置文件解析这套 grep 加正则的思路不光能用来抓系统版本任何KEYvalue格式的配置文件都能用。比如抓某个服务的配置文件里的端口号grep -oP ^port\K[0-9] /etc/myservice.conf或者抓日志里的 IP 地址grep -oP \d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3} access.log核心思路是一样的用-o只输出匹配部分用\K丢掉前缀用字符类精确控制匹配范围。掌握这一个套路能解决一大类文本提取问题。6.5 性能与可读性的平衡最后说个经验。正则写得太复杂可读性会急剧下降。我见过有人写一长串正则自己过两天都看不懂。这种情况下宁可拆成几步用管道串起来每一步都简单清晰。比如抓版本号与其写一个复杂的 lookbehind 加 lookahead不如grep ^VERSION_ID /etc/os-release | cut -d -f2 | tr -d 三步每步都一目了然。虽然多了一次管道但对于配置文件这种小文件性能差异可以忽略。可维护性比那点性能重要得多。我在实际写运维脚本的时候越来越倾向于“简单直白”而不是“一行流”。因为脚本是要长期维护的三个月后回来看能一眼看懂比省那几毫秒有价值得多。正则表达式是个好工具但别为了炫技把它用得太复杂。够用、能读懂、好排查这三点比什么都重要。