ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

Shell变量与字符串操作从入门到避坑:赋值、引用、截取、替换与比较全解析

Shell变量与字符串操作从入门到避坑:赋值、引用、截取、替换与比较全解析 1. 为什么值得花时间把Shell变量和字符串吃透很多人第一次写Shell脚本都是从一行echo hello world开始的。写完之后觉得“不过如此”然后真正上手干活时立刻被各种问题打脸变量赋值多了空格报错、字符串里带空格被拆成好几段、$var和${var}混着用结果输出完全不是自己想要的、明明变量有值却判断为空……这些坑几乎每个写Shell的人都踩过。Shell脚本入门这件事变量和字符串是绕不过去的第一道坎。它不像Python那样有严格的类型系统帮你兜底也不像Java那样编译器会提前告诉你哪里写错了。Shell的变量是弱类型的字符串和数字经常混在一起用语法又极其依赖空格和引号稍不留神就是“语法正确但结果错误”的隐蔽bug。所以我把变量和字符串单独拎出来做一次系统梳理把赋值、引用、拼接、截取、替换、比较这些高频操作讲透同时把那些文档里不会写、只有实际踩过才知道的坑一并说清楚。这篇文章适合刚接触Shell脚本、能看懂基本命令但一写脚本就出问题的朋友也适合写了几年脚本但一直靠“试出来”而不是“想明白”的从业者。我会尽量用生活化的类比解释原理用可直接复制的代码演示操作让你看完就能用用了不出错。2. Shell变量的本质与赋值规则拆解2.1 变量到底是什么一个带名字的盒子你可以把变量理解成一个贴了标签的盒子。标签就是变量名盒子里装的东西就是变量值。在Shell里这个盒子对“装什么”几乎没有限制你可以装字符串、装数字、装命令的输出结果甚至装一个空值。Shell不关心盒子里装的是什么类型它统一按字符串来处理需要做数学运算时再临时转换。这一点和Python、C语言完全不同。C语言定义int a 10;之后这个盒子只能装整数你塞个字符串进去编译器直接报错。Shell没有这个约束a10和ahello在语法上是一样的都是赋值。理解这一点非常关键因为后面很多“奇怪”的行为都源于此。变量名有命名规范只能包含字母、数字和下划线且不能以数字开头。my_var、_count、name2都是合法的2name、my-var不合法。虽然Shell对大小写敏感Name和name是两个不同的变量但行业惯例是变量名统一用大写表示环境变量或常量小写表示脚本内部的局部变量这样读代码时一眼就能区分。2.2 赋值的三条铁律等号两边不能有空格这是新手最容易犯的错误没有之一。看下面三行name zhangsan # 错误 name zhangsan # 错误 namezhangsan # 正确第一种写法Shell会把name当成一个命令去执行然后发现找不到这个命令报command not found。第二种写法Shell会把name当成一个临时环境变量赋值给后面的命令zhangsan同样报错。只有第三种等号紧贴变量名和值才是正确的赋值语法。为什么Shell要这么设计因为Shell最初是一个命令解释器它的核心工作是“执行命令”。name zhangsan在Shell眼里就是“执行名为name的命令参数是和zhangsan”。等号两边不留空格是Shell区分“赋值”和“执行命令”的方式。记住这条规则能帮你省下大量排查时间。提示如果你确实需要在赋值时保留空格比如值本身包含空格必须用引号把值包起来例如namezhang san。2.3 单引号、双引号、反引号的区别与选择赋值时值可以用引号包裹也可以用引号但不同引号的行为差异很大这是Shell字符串处理的核心知识点。单引号是“所见即所得”里面写什么就是什么变量不会被展开特殊字符也不会被解释。双引号允许变量展开和命令替换但会保留大部分特殊字符的字面含义。反引号用于命令替换把命令的输出结果作为值不过现在更推荐用$()写法因为反引号在嵌套时容易出错。nameworld echo hello $name # 输出 hello $name echo hello $name # 输出 hello world echo today is $(date %F) # 输出 today is 2025-01-15选择哪种引号取决于你是否希望变量被展开。如果值里包含$、反引号、反斜杠这些字符而你希望它们保持字面含义就用单引号。如果值里需要嵌入变量或命令输出就用双引号。我个人的习惯是除非明确需要变量展开否则一律用双引号因为双引号能防止值中的空格导致意外的单词拆分这个坑在后面讲字符串时会详细展开。2.4 环境变量与局部变量的作用域差异Shell变量按作用域分两类环境变量和局部变量。环境变量用export导出后会被当前Shell及其所有子进程继承。局部变量只在当前Shell进程中有效子进程看不到。my_varlocal export my_envglobal bash -c echo $my_var # 输出空行 bash -c echo $my_env # 输出 global这个差异在实际写脚本时非常重要。如果你在脚本里定义了一个变量然后调用另一个脚本或命令期望那个脚本能读到这个变量就必须用export。反过来如果你不希望变量污染子进程的环境就不要导出。查看当前所有环境变量用env或printenv查看所有变量包括局部变量用set。这两个命令在排查“变量为什么没传进去”这类问题时非常有用。3. 字符串操作的核心技法与高频场景3.1 字符串拼接三种写法与性能考量Shell的字符串拼接非常直接把两个字符串挨着写就行不需要像Java那样用号也不需要像Python那样用join。firsthello secondworld result1$first $second # 双引号内直接拼接 result2${first}${second} # 用花括号明确边界 result3$first$second # 不加引号也能拼接三种写法都能得到拼接结果但推荐用第二种${first}${second}。原因是花括号明确了变量名的边界避免歧义。比如你想把变量$name和字符串s拼在一起写成$names会被Shell理解成变量names而${name}s才是你想要的。这个细节在批量生成文件名、构造路径时特别容易踩坑。不加引号的拼接在值不含空格时没问题但一旦值里有空格Shell会做单词拆分结果就不可控了。所以我的建议是拼接时始终用双引号包裹变量用花括号界定边界。3.2 字符串长度一个井号搞定获取字符串长度用${#var}这是Shell内置的能力不需要调用外部命令效率很高。strhello world echo ${#str} # 输出 11注意这里的花括号不能省$#str是另一个意思$#表示脚本参数个数后面跟str就是普通文本。这个语法在判断输入是否为空、校验字段长度时非常实用。如果字符串包含多字节字符比如中文${#str}返回的是字符数还是字节数取决于当前locale设置。在UTF-8环境下通常返回字符数但为了保险处理中文时建议实测确认。3.3 字符串截取从指定位置取子串Shell内置了字符串截取语法格式是${var:offset:length}offset是起始位置从0开始length是要取的长度省略length则取到末尾。strabcdefghij echo ${str:0:3} # 输出 abc echo ${str:3} # 输出 defghij echo ${str: -3} # 输出 hij注意负号前有空格负数偏移表示从末尾往前数但负号前面必须有一个空格否则Shell会把它当成默认值语法${var:-default}来解析。这个坑很隐蔽我第一次用时排查了半天。截取操作在处理日志、解析固定格式的字符串比如日期20250115取年月日时特别方便不用调用cut或awk纯Shell就能完成。3.4 字符串替换批量修改的利器替换语法有两种${var/old/new}替换第一个匹配${var//old/new}替换所有匹配。path/home/user/docs/file.txt echo ${path/user/admin} # 输出 /home/admin/docs/file.txt echo ${path//\//-} # 输出 -home-user-docs-file.txt第二个例子把路径中所有的斜杠替换成短横线//表示全局替换\/是对斜杠做转义。这个技巧在生成文件名、构造URL时很常用。如果new部分留空就是删除匹配内容${path// /}可以删除字符串中所有空格。这比用sed或tr更轻量而且不启动子进程性能更好。3.5 字符串比较、与-eq的边界字符串比较用或数值比较用-eq、-ne、-lt、-gt等。混用会导致意想不到的结果。a10 b9 if [ $a $b ]; then echo 相等; fi # 字符串比较不相等 if [ $a -eq $b ]; then echo 数值相等; fi # 数值比较不相等 if [ $a -gt $b ]; then echo a大于b; fi # 数值比较109成立如果用或做字符串比较在[ ]中会被当成重定向符号必须写成\或在[[ ]]中使用。这也是为什么推荐用[[ ]]而不是[ ]前者支持更多语法且不容易出错。注意比较时变量一定要加双引号否则变量为空时[ $a $b ]会变成[ ]直接报语法错误。4. 从零写一个字符串处理脚本完整实操记录4.1 需求定义与脚本框架设计假设我们要写一个脚本处理一批用户提交的邮箱地址完成以下任务去除首尾空格、统一转成小写、提取用户名和域名部分、统计每个域名的出现次数。这个需求在数据清洗场景中很典型。脚本框架设计如下先定义一个函数处理单个邮箱返回规范化后的结果然后遍历输入列表调用函数处理最后用关联数组统计域名频次。整个脚本不依赖外部命令纯Shell实现保证可移植性。#!/bin/bash normalize_email() { local email$1 email${email#${email%%[![:space:]]*}} email${email%${email##*[![:space:]]}} email${email,,} echo $email }这里用到了两个不常见的语法${email%%[![:space:]]*}用于去除开头的空白字符${email##*[![:space:]]}用于去除末尾的空白字符。${email,,}是Bash 4.0以上版本支持的转小写语法。如果你的环境是较老的Bash可以用tr [:upper:] [:lower:]替代。4.2 参数校验与默认值处理脚本入口处需要做参数校验确保用户传入了必要的参数。Shell提供了${var:-default}语法设置默认值${var:?message}在变量为空时报错退出。input_file${1:?请提供输入文件路径} output_file${2:-/tmp/result.txt}第一行表示如果$1为空就打印错误信息并退出。第二行表示如果$2为空就用/tmp/result.txt作为默认值。这两个语法在写健壮的脚本时非常有用能避免变量为空导致的后续错误。参数校验还包括检查文件是否存在、是否可读if [[ ! -f $input_file ]]; then echo 文件不存在: $input_file 2 exit 1 fi注意错误信息输出到2标准错误这是好习惯方便调用者区分正常输出和错误信息。4.3 逐行处理与域名统计实现读取文件用while read循环配合IFS防止行首尾空格被吃掉declare -A domain_count while IFS read -r line; do [[ -z $line ]] continue email$(normalize_email $line) domain${email#*} ((domain_count[$domain])) done $input_file${email#*}表示删除第一个及其左边的所有内容剩下的就是域名部分。declare -A声明关联数组用域名作为key统计次数。((domain_count[$domain]))是算术运算语法对数组元素做自增。最后输出统计结果for domain in ${!domain_count[]}; do printf %-20s %d\n $domain ${domain_count[$domain]} done | sort -k2 -rn${!domain_count[]}获取所有keysort -k2 -rn按第二列数值降序排列。整个脚本跑下来一个几十行的Shell脚本就完成了数据清洗和统计的核心功能。4.4 脚本执行与结果验证准备一个测试文件emails.txtZhangSanExample.COM LiSitest.org wangwuEXAMPLE.com zhaoliutest.org执行脚本./process.sh emails.txt预期输出example.com 2 test.org 2验证要点首尾空格是否被去除、大小写是否统一、域名统计是否准确。如果结果不对用bash -x process.sh emails.txt开启调试模式逐行查看变量值的变化这是排查Shell脚本问题最有效的手段。5. 常见问题与排查技巧实录5.1 变量为空导致的判断失效这是最高频的问题。看下面这段代码if [ $name admin ]; then echo welcome fi如果$name为空这行会变成[ admin ]Shell报unary operator expected。解决方法很简单变量加双引号。if [ $name admin ]; then双引号保证即使变量为空也会变成[ admin ]语法正确判断结果为false。这个习惯要刻进肌肉记忆里所有变量引用都加双引号除非你明确知道自己在做什么。5.2 字符串包含空格时的单词拆分陷阱filesa.txt b.txt c.txt for f in $files; do echo $f done这段代码能正常输出三个文件名因为Shell对未加引号的$files做了单词拆分。但如果你希望把整个字符串当成一个整体就会出问题msghello world if [ $msg hello world ]; then # 报错参数过多$msg被拆成hello和world两个参数[命令收到三个参数报too many arguments。加双引号即可解决。记住变量引用加双引号是Shell脚本的第一安全准则。5.3 命令替换中的换行符丢失files$(ls) echo $files$(ls)会把多行输出合并成一行换行符被替换成空格。如果你需要保留换行用while read循环ls | while read -r f; do echo 文件: $f done或者临时修改IFS。这个坑在处理文件列表、日志行时经常遇到理解命令替换会做“单词拆分和换行合并”是解决问题的关键。5.4 常见问题速查表问题现象根本原因解决方法command not found赋值时等号两边有空格去掉空格varvalueunary operator expected变量为空且未加引号变量引用加双引号too many arguments字符串含空格被拆分变量引用加双引号变量在子进程中读不到未用export导出赋值后加export var${var: -3}报错负号前缺空格写成${var: -3}中文长度计算不对locale设置问题检查LANG环境变量替换只生效一次用了单斜杠改用双斜杠${var//old/new}5.5 几个我踩过的坑和私房技巧第一个坑在[[ ]]里用做模式匹配时右边的模式不要加引号否则会变成字面比较。[[ $name zhang* ]]是模式匹配[[ $name zhang* ]]是字面比较结果完全不同。第二个技巧用${varQ}可以输出变量的引号转义形式调试时特别有用能看清变量里到底有什么不可见字符。第三个技巧处理用户输入时先用read -r读取再用[[ $input ~ ^[a-zA-Z0-9.]$ ]]做格式校验正则表达式在[[ ]]中直接支持不需要调用grep。第四个坑local关键字只能在函数内使用在函数外写local var1会报错。而且local声明的变量在函数返回后就失效不要指望它能跨函数传递。6. 变量与字符串的进阶用法与性能建议6.1 间接引用与变量名动态构造有时候你需要通过一个变量的值来访问另一个变量这叫间接引用。Bash提供了${!var}语法namezhangsan keyname echo ${!key} # 输出 zhangsan这个技巧在遍历配置项、动态构造变量名时很有用。但要注意间接引用会降低代码可读性能用数组解决的就用数组不要滥用。6.2 大小写转换与字符类操作Bash 4.0以上支持${var^^}转大写、${var,,}转小写。如果环境不支持用tr命令替代lower$(echo $str | tr [:upper:] [:lower:])字符类替换也很实用${var//[0-9]/}删除所有数字${var//[^a-zA-Z]/}只保留字母。这些操作在数据清洗时能省下大量sed调用。6.3 什么时候该用外部命令什么时候纯Shell纯Shell的字符串操作不启动子进程性能远高于sed、awk、cut。但纯Shell的语法可读性差复杂逻辑写起来容易出错。我的经验是简单的截取、替换、长度计算用纯Shell复杂的正则匹配、多字段解析用awk或sed。不要为了“纯Shell”而牺牲可维护性脚本是给人看的顺便给机器执行。6.4 脚本调试的三种有效手段第一种bash -x script.sh打印每条执行的命令和变量展开后的值最直观。第二种在关键位置插入echo DEBUG: var$var 2输出到标准错误不影响正常输出。第三种用set -e让脚本遇到错误立即退出用set -u让引用未定义变量时报错这两个选项能帮你提前发现很多隐藏问题。注意set -e在某些场景下会失效比如命令在if条件中、在或||连接中。不要完全依赖它关键步骤还是要显式检查返回值。6.5 写可维护Shell脚本的几条个人原则变量名要有意义不要用a、b、c用user_name、file_path这种自解释的名字。所有变量引用加双引号这是铁律。函数内的变量用local声明避免污染全局。脚本开头写set -euo pipefail让脚本在严格模式下运行。复杂逻辑拆成函数每个函数只做一件事。注释写“为什么”而不是“做什么”代码本身能说明做什么注释要说明为什么这么做。这些原则看起来简单但坚持下来能让你的脚本从“能跑”变成“可靠”。我见过太多脚本因为一个未加引号的变量在生产环境跑了半年后突然出错排查成本远超当初多写几个引号的时间。
返回列表