POSIX Shell中可移植删除指定空格分隔单词的方法及循环优化
问题
我定义了这样的变量:
var1="word1 word2 word3"
想知道有没有可移植的Shell方式,能按空格分隔截取/删除其中的单词?需求是当命令带--ignore选项及对应参数时:
$ cmd --ignore word1 # 场景1 $ cmd --ignore "word1 word2" # 场景2
希望var1分别变成:
"word2 word3" # 场景1结果 "word3" # 场景2结果
如果没法实现这个需求,能不能优化下面这个for循环的执行效率?我本来打算用这个循环实现类似效果:
# 先通过while循环解析选项,把--ignore的参数赋值给$sh_ignore for i in $var1 do # 反过来检查$i是否在$sh_ignore里,避免$sh_ignore有多个单词时匹配出错 if ! echo "$sh_ignore" | grep "$i"; then # 正常执行操作 else # 跳过当前单词 fi done
更新内容
查资料并参考@chepner的评论后,我暂时写了下面的代码,希望得到改进建议:
sh_ignore='' while :; do case $1 in # 其他选项处理逻辑 --ignore) if [ "$2" ]; then sh_ignore=$2 shift else # die是我定义的函数:打印错误信息后以状态码1退出 die 'ERROR: "--ignore" requires a non-empty option argument.' fi ;; # 处理--ignore不带参数的情况 # 处理--和未知选项 esac shift done if [ -n "$sh_ignore" ]; then for d in $sh_ignore do var1="$(echo "$var1" | sed -e "s,$d,,")" done fi # 后续用处理后的$var1循环执行操作 for i in $var1 do # 正常执行操作 done
解决方案与优化建议
一、可移植实现删除指定单词的方法
完全可以用POSIX兼容的方式实现需求,这里提供两种可靠方案:
方法1:基于sed的批量替换(纯POSIX兼容)
把要忽略的单词拼接成完整的正则规则,一次性完成所有删除,避免多次调用外部命令:
# 保持你原有的选项解析逻辑得到sh_ignore if [ -n "$sh_ignore" ]; then # 将每个忽略单词转为带边界的正则,避免部分匹配(比如word1不会匹配word12) ignore_pattern=$(echo "$sh_ignore" | sed -e 's/ /\\|\\</g' -e 's/^/\\</' -e 's/$/\\>/') # 批量删除匹配单词,同时清理多余空格 var1=$(echo "$var1" | sed -e "s/$ignore_pattern//g" -e 's/^ *//' -e 's/ *$//' -e 's/ */ /g') fi
\<和\>是POSIX sed支持的单词边界匹配符- 最后三个sed命令分别清理开头空格、结尾空格、中间连续空格,保证结果格式整洁
方法2:纯Shell数组处理(兼容bash/ksh等支持数组的Shell)
如果目标环境支持数组,这个方法完全不需要外部命令,效率更高:
# 把原变量和忽略参数转为数组 read -ra var_arr <<< "$var1" read -ra ignore_arr <<< "$sh_ignore" # 过滤掉要忽略的单词 new_var1="" for word in "${var_arr[@]}"; do skip=0 for ignore_word in "${ignore_arr[@]}"; do if [ "$word" = "$ignore_word" ]; then skip=1 break fi done [ $skip -eq 0 ] && new_var1="$new_var1 $word" done # 清理开头多余空格 var1=${new_var1# }
二、优化你最初的for循环
原循环频繁调用echo+grep创建子进程,效率很低,改成Shell内置匹配即可大幅提升速度:
# 给忽略列表前后加空格,确保匹配完整单词 ignore_list=" $sh_ignore " for i in $var1; do # 用Shell内置的[[ ]]做字符串匹配,无需外部命令 if [[ "$ignore_list" != *" $i "* ]]; then # 正常执行操作 else # 跳过当前单词 fi done
三、对你更新后代码的改进建议
你的代码有几个需要修正和优化的点:
- 避免误匹配:当前
sed -e "s,$d,,")会匹配任意位置的$d(比如d=word会删掉word1里的word),要加上单词边界:
sed -e "s/\<$d\>//g"
- 减少sed调用次数:循环调用sed会多次创建子进程,改成一次性批量处理:
if [ -n "$sh_ignore" ]; then # 构建匹配所有忽略单词的正则 ignore_regex=$(echo "$sh_ignore" | sed 's/[^ ]*/\\<&\\>/g; s/ /\\|/g') # 一次性替换+清理空格 var1=$(echo "$var1" | sed -e "s/$ignore_regex//g" -e 's/^ *//; s/ *$//; s/ */ /g') fi
选项解析的小修正:
case语句必须指定匹配对象,要写成case $1 in,否则无法正确解析参数。支持带空格的忽略单词(可选):如果需要
--ignore接受带空格的单词(比如--ignore "hello world"),要改用数组保存参数,避免被拆分:
# 选项解析部分修改 --ignore) if [ "$2" ]; then # 用数组保存参数,保留带空格的单词 read -ra sh_ignore_arr <<< "$2" shift else die 'ERROR: "--ignore" requires a non-empty option argument.' fi ;; # 后续处理逻辑 if [ "${#sh_ignore_arr[@]}" -gt 0 ]; then ignore_regex="" for d in "${sh_ignore_arr[@]}"; do # 转义正则特殊字符,避免/&等符号报错 escaped_d=$(sed 's/[\/&]/\\&/g' <<< "$d") ignore_regex="$ignore_regex\\<$escaped_d\\>|" done # 去掉最后多余的| ignore_regex=${ignore_regex%|} var1=$(echo "$var1" | sed -e "s/$ignore_regex//g" -e 's/^ *//; s/ *$//; s/ */ /g') fi
内容的提问来源于stack exchange,提问作者jimmymcheung
相关产品推荐
相关产品推荐

