shell 未处理评估失败

noj0wjuj  于 2023-08-07  发布在  Shell
关注(0)|答案(2)|浏览(87)

我正在编写一个在BASH脚本中使用的配置文件解析器。它具有变量赋值的基本shell语法,但也有一些额外的验证以避免意外误用。一切都在一个成功的场景中运行。然而,当eval失败时,不执行处理失败的方法。我不知道为什么不能处理。下面我做了一个简化的例子来演示我遇到的问题。

#!/usr/bin/env bash
shopt -s -o nounset
shopt -s -o errexit
shopt -s -o pipefail
shopt -s globstar
shopt -s extglob
#shopt -s -o xtrace
IFS=$'\n\t'
 
 
function loadConf()
{
    local confFileLcl="$1"
    local lineLcl
    local optionLcl
    local valueLcl
 
    while IFS='' read -r lineLcl
    do
        lineLcl=${lineLcl%%#*}
 
        case "${lineLcl}" in
        *=*)
            optionLcl="${lineLcl%%=*}"
            valueLcl=${lineLcl#*=}
            valueLcl=${valueLcl//\"/}
            eval "${optionLcl}='${valueLcl}'" || echo  "Failed to set '${optionLcl}'."
        esac
    done <"${confFileLcl}"
}
declare -frt loadConf
 
# Create configuration file
echo -e "
var0=\"Value is set\"
var1=\"This is not an integer\"
" > "test.conf"
 
# Create variable that is to be configured to succeed.
declare var0="not-set"
# Create variable that is to be configured to fail.
declare -i var1=42
 
# Load the configuration file
loadConf "test.conf"
 
[[ "${var0}" == "Value is set" ]] || echo "Var0 check failed."
[[ "${var1}" -eq 42 ]] || echo "Var1 check failed."

字符串
我已经尝试了各种方法来缩小问题的范围,包括禁用启用的严格模式。这并不会对eval失败处理产生影响,只是打印的错误消息的类型不同。
执行输出:

$ ./demo.sh

./demo.sh:line 27:This:非约束变量

第27行是:eval“${optionLcl}='${valueLcl}'”||{ echo“无法设置“${optionLcl}”。”; true;
我对输出的期望是:
无法设置“var 1”。
Var 1检查失败。
使用跟踪运行:

$ ./demo.sh                               
+ IFS='                                   
        '                                 
+ declare -frt loadConf                   
+ echo -e '                               
var0="Value is set"                       
var1="This is not an integer"             
'                                         
+ declare var0=not-set                    
+ declare -i var1=42                      
+ loadConf test.conf                      
+ local confFileLcl=test.conf             
+ local lineLcl                           
+ local optionLcl                         
+ local valueLcl                          
+ IFS=                                    
+ read -r lineLcl                         
+ lineLcl=                                
+ case "${lineLcl}" in                    
+ IFS=                                    
+ read -r lineLcl                         
+ lineLcl='var0="Value is set"'           
+ case "${lineLcl}" in                    
+ optionLcl=var0                          
+ valueLcl='"Value is set"'               
+ valueLcl='Value is set'                 
+ eval 'var0='\''Value is set'\'''        
++ var0='Value is set'                    
+ IFS=                                    
+ read -r lineLcl                         
+ lineLcl='var1="This is not an integer"' 
+ case "${lineLcl}" in                    
+ optionLcl=var1                          
+ valueLcl='"This is not an integer"'     
+ valueLcl='This is not an integer'       
+ eval 'var1='\''This is not an integer'\'
++ var1='This is not an integer'          
./demo.sh: line 27: This: unbound variable

dnph8jn4

dnph8jn41#

现在,您的跟踪显示了正在发生的事情。如果你写的是

eval "${optionLcl}=foo"

字符串
这里的问题是optionLcl扩展为var1,并且您已经声明var1为整数,这意味着右侧是在数值上下文中计算的。
在这种情况下,是一个任务

var1=foo


被解释为(因为foo显然不是一个数字)

var=$foo


这类似于echo $((a+b)),它被解释为echo $(($a + $b))。但是没有名为foo的变量(或者,在您的例子中,没有名为This的变量,因为在您的原始程序中,赋值是 This is not an integer)。因为您还打开了 unset 选项,所以这会杀死您。
问题仍然存在,为什么在eval遇到错误后不执行|| echo ...分支。之后,一个简单的语法错误,如

eval ')'  || echo error


显示 error(除了来自eval的错误消息)。一些实验表明,numeric 上下文中的赋值行为似乎与我们预期的不同。范例:

eval "abc=$((1+))" || echo error


只显示语法错误,但不显示 error。赋值或参数替换似乎是关键点:

eval ": $((1+))" || echo error   # Param expansion. Does NOT print "error"
eval "((1+))" || echo error      # No expansion. DOES print "error"


我不知道这种行为上的差异是bash的一些模糊但有文档记录的特性,还是仅仅是一个bug。

变通办法

因为你不能捕捉到将非数值的东西赋值给数值变量的错误(删除 unset 只会让这个错误不被发现,而拥有 unset 会中止你的程序),我的建议是将检查与实际赋值分开。你可以做个

if eval "( ${optionLcl}='${valueLcl}' )"
then
  eval "${optionLcl}='${valueLcl}'"
else
  echo  "Failed to set '${optionLcl}'."
fi


第一个eval使用子shell,因此错误不会向上传播,除非通过子shell的退出代码,该代码由if捕获。但是,由于eval发生在子进程中,因此脚本中不会设置任何变量。因此,我们在脚本的then分支中重复eval,因为我们现在知道我们是安全的。

wgeznvg7

wgeznvg72#

nounset选项与set -u相同,并不意味着“导致对未设置变量求值的命令失败”。它的意思是“如果访问未设置的变量,则完全终止非交互式脚本”。所以没有其他命令有机会执行。交互式shell不会退出;在内部,它将执行类似于longjmp的操作,以跳出任何递归并返回到提示符。
解决这个问题的方法是检查正在设置的变量,如果没有设置,则避免eval
要检查是否设置了变量var,我们可以使用语法${var+y}。如果设置了var(可能为空,但已设置),则会扩展为y。如果var不存在,它将扩展为空。
如果名称var保存在变量varname中,我们可以用eval来实现:

eval "exists=\${$varname+y}"

if [ $exists ] ; then
  eval "$othervarname=\$$varname"
else
  printf "%s: undefined variable\n" $varname
fi

字符串
我在这里假设$varname已经被验证为一个有效的变量名,这样的事情不需要引号。

相关问题