返回

Bash 脚本安全计算 Fibonacci 数列及命令替换详解

Linux

Bash 脚本中 Fibonacci 数列计算及命令替换

Fibonacci 数列是一个经典的数学问题,其定义为:除了第一个和第二个数字为 1 之外,其他每个数字都是前两个数字之和。在编写脚本时,难免会遇到使用这个算法的时候。本文重点讨论用 bash 脚本解决 Fibonacci 数列的问题。同时重点讲解如何在 bash 脚本中安全有效地进行命令替换。

Fibonacci 脚本分析与优化

用户需要将原脚本中算术表达式 $(($n0+$n1)) 替换为命令 expr $n0 + $n1,实现同样的功能,同时保留向变量赋值的行为。

原脚本存在一些可以改进的地方。用户提供的代码使用 ‘seq 1 $1‘ 在引号中导致变量 $1 无法正确展开,进而可能导致 seq 命令的无效使用,导致计算出错或者运行异常,应该修改为双引号,例如:"seq 1 $1",并确认seq在shell环境中被安装和正确识别。

原脚本的流程已经实现了计算 Fibonacci 数列。seq 命令生成一个数字序列,然后通过 for 循环对每个数字进行计算。其中,变量 n0n1 分别保存前两个数字,变量 n 保存当前数字。如果发现前两个变量没有赋值过,且当前的 c 不为 1、2,则进入不到对应的计算逻辑中,导致计算的最终结果错误。原脚本可以通过对 $c 等于 3 进行一次特判进行修复,以保证前两个计算被执行。for循环的边界和条件的判断决定了脚本的输出内容,使用 $1 对脚本传入计算的数量,用户可以使用和修改这个脚本进行测试和实验。
建议对 $1 进行合法性校验。

#!/bin/bash
if (test $# -ne 1)
then
  echo "Use: $0 number"
  exit 1
fi

# 添加对 $1 是否为正整数的判断
if ! [[ "$1" =~ ^[0-9]+$ ]] || [ "$1" -eq 0 ]; then
  echo "Error: Argument must be a positive integer."
  exit 1
fi

for c in `seq 1 $1` #修改为正确的用法
do
  if (($c == 1))
  then
    n0=1;
    echo "fibonacci(1)=$n0";
  elif (($c == 2));
  then
    n1=1;
    echo "fibonacci(2)=$n1";
  elif (($c == 3));
  then
    n=$(($n0+$n1)); #修复c为3的情况
    echo "fibonacci($c)=$n";
    n0=$n1;
    n1=$n;
  else
    n=$(($n0+$n1));
    echo "fibonacci($c)=$n";
    n0=$n1;
    n1=$n;
  fi
done

针对 $c 等于 1 和 2 的情况可以使用数组提前设定。例如 arr=(0,1,1)

Bash 脚本中的命令替换

Bash 脚本中的命令替换允许捕获一个命令的输出,并将其作为变量的值或者用在另一个命令中。两种常用的命令替换方式:

  1. 反引号 (`)
  2. $()

使用 expr 命令和命令替换计算 Fibonacci

使用 expr 命令进行算术运算并替换变量。要捕获 expr $n0 + $n1 的输出,可以这样操作:

方案一:反引号 + expr

直接把 expr $n0 + $n1 放到反引号里,将执行结果赋给 n

操作步骤:

  1. $n0$n1 作为 expr 命令的参数,用于计算它们的和。
  2. 用反引号 expr n0 + n1` 括起来,执行命令并将标准输出结果替换到当前位置。
  3. n= 开头,将后面的值赋给 n 变量。
#!/bin/bash
if (test $# -ne 1)
then
  echo "Use: $0 number"
  exit 1
fi

# 添加对 $1 是否为正整数的判断
if ! [[ "$1" =~ ^[0-9]+$ ]] || [ "$1" -eq 0 ]; then
  echo "Error: Argument must be a positive integer."
  exit 1
fi

for c in `seq 1 $1` #修改为正确的用法
do
  if (($c == 1))
  then
    n0=1;
    echo "fibonacci(1)=$n0";
  elif (($c == 2));
  then
    n1=1;
    echo "fibonacci(2)=$n1";
  elif (($c == 3));
  then
    n=`expr $n0 + $n1`; #通过反引号将expr的输出结果给n
    echo "fibonacci($c)=$n";
    n0=$n1;
    n1=$n;
  else
    n=`expr $n0 + $n1`;
    echo "fibonacci($c)=$n";
    n0=$n1;
    n1=$n;
  fi
done
方案二:$() + expr

expr $n0 + $n1 放到 $() 中。这种方式与反引号在功能上类似。

操作步骤:

  1. 和方案一大体相同。
  2. 把需要执行的命令换成 $()
#!/bin/bash
if (test $# -ne 1)
then
  echo "Use: $0 number"
  exit 1
fi

# 添加对 $1 是否为正整数的判断
if ! [[ "$1" =~ ^[0-9]+$ ]] || [ "$1" -eq 0 ]; then
  echo "Error: Argument must be a positive integer."
  exit 1
fi

for c in `seq 1 $1` #修改为正确的用法
do
  if (($c == 1))
  then
    n0=1;
    echo "fibonacci(1)=$n0";
  elif (($c == 2));
  then
    n1=1;
    echo "fibonacci(2)=$n1";
  elif (($c == 3));
  then
    n=$(expr $n0 + $n1); #通过$()将expr的输出结果给n
    echo "fibonacci($c)=$n";
    n0=$n1;
    n1=$n;
  else
    n=$(expr $n0 + $n1);
    echo "fibonacci($c)=$n";
    n0=$n1;
    n1=$n;
  fi
done

注意: expr 命令在处理变量时,如果变量值为空或未定义,可能会导致错误。为防止用户篡改或者导致脚本注入,例如当 $n0$n1 包含特殊字符或命令时,可能发生执行了预期外命令的情况。为防止篡改,可以在之前对每个计算执行一次有效性校验。确保其为纯数字,可以在每次计算之前使用 if ! [[ "$n0" =~ ^[0-9]+$ ]] 校验两个输入是否正确。如果数字可能产生越界,在一些版本较低的 expr 中可能无法使用计算结果。可以自行开发新的逻辑或使用例如 bc 等其他运算程序完成计算。expr 输出的结尾会多一个空格,根据输出情况决定是否保留或者删去该空格。

通过 $() 的方式,可以将一些逻辑复杂的命令进行封装,并在shell环境内执行,这为我们的工作提供了非常多的便利。

安全的最佳实践:

进行命令替换时,特别是当命令的参数来源于外部输入时,务必小心安全问题。错误的输入可能导致意外的行为,甚至安全漏洞。

  1. 输入验证: 对所有外部输入的参数进行合法性验证。在这个例子中,确保 $n0$n1 是数字。

    if ! [[ "$n0" =~ ^[0-9]+$ ]] || ! [[ "$n1" =~ ^[0-9]+$ ]]; then
      echo "Error: Invalid input. n0 and n1 must be integers."
      exit 1
    fi
    
  2. 使用双引号: 当展开变量时,始终用双引号将变量引用括起来。这能防止单词分割和通配符扩展引起问题。在命令替换中使用时也是一样的:n=$(expr "$n0" + "$n1")

  3. 尽可能不使用外部输入计算变量: 尝试使用bash内置的运算符直接在bash中计算。例如用户的问题可以写作 $((n0 + n1)) 。因为当用户可以传递任意参数给脚本时,这些变量被使用的危险性直线上升,并且通过一定的手段和条件能轻易的造成命令注入问题,从而让整个设备收到严重的入侵,务必避免在任何计算中使用用户的输入计算。

使用重定向保存到文件(方案三)

用户提到了将输出保存到文件,但此例中不必如此。若有此需求,可以通过以下方法实现:

操作步骤:

  1. 使用expr $n0 + $n1 > output.txt,命令结果输出到文件而不是直接替换。
  2. 使用例如 $((cat output.txt)) 将文件结果重新读取回bash,再次完成替换,为 n 变量赋值。
  3. rm output.txt 可以删去不需要的文件,避免服务器中内容被不断累积。
#!/bin/bash
if (test $# -ne 1)
then
  echo "Use: $0 number"
  exit 1
fi

if ! [[ "$1" =~ ^[0-9]+$ ]] || [ "$1" -eq 0 ]; then
  echo "Error: Argument must be a positive integer."
  exit 1
fi

for c in `seq 1 $1`
do
  if (($c == 1))
  then
    n0=1;
    echo "fibonacci(1)=$n0";
  elif (($c == 2));
  then
    n1=1;
    echo "fibonacci(2)=$n1";
  elif (($c == 3));
  then
    expr $n0 + $n1 > output.txt;
    n=$((`cat output.txt`));
    rm output.txt
    echo "fibonacci($c)=$n";
    n0=$n1;
    n1=$n;
  else
    expr $n0 + $n1 > output.txt;
    n=$((`cat output.txt`));
    rm output.txt
    echo "fibonacci($c)=$n";
    n0=$n1;
    n1=$n;
  fi
done

尽管上述方案也可以做到输出并保存计算结果给 n。但是这个方法并不推荐使用。频繁读写文件会降低性能,尤其是当这个脚本频繁被使用的时候。尽量用 n=$(expr $n0 + $n1)这种将命令直接输出作为中间的计算的办法进行处理。用户可以通过将原本输出到 output.txt 中的方式修改为直接将 echo $n 命令的结果输出,就可以将每一个结果保存在 output 文件中。这比直接执行 n=$(expr $n0 + $n1)多出了一次文件 IO,务必谨慎使用。