简单的 Bash CLI 程序 & 简单的元编程

简单的前言

虽然这里的日期是 2024-04-03,但是我写这篇博客的时候其实是 2025-02-18,写 2025-02-17 那篇博客的时候,提到 polyglot 相关的内容,这部分内容其实是上上个学期的计算机系统的讨论课的产出,于是想着把沉睡在 …/Downloads,…/Courses/CS24Spring,小组群文件等目录里的讨论课材料里有点意思的部分翻出来晾晾,所以这一页博客出现在你的眼前.

我看到相当多的说法说 bash 的语法很不舒服,然而我在接触到这种说法之前,已然习惯了 bash 的语法,“我比流言蜚语先认识你”??😶‍🌫️

这篇博客相关的讨论课的题目是——对于 C 中的 switch-case,编译器的行为如何,即 case 呈现什么样的分布(连续/不连续,间隔大小)时,编译器将生成跳转表. 既然要探讨不同 case 对应的情形,那么首先要生成不同 case 对应的 C 源文件,这个事情疑似有点机械了,那么考虑用自动化的脚本去完成. 我们对这个脚本的预期是 generator -b 10 -s 2 -d dest_dir -f file_name 将产生分支数量(-b)为 10,分支间隔(-s)为 2,存放目录(-d)为 dest_dir,文件名(-f)为 file_name.c,有了单个文件的生成器,我们可以再写一个生成器来调用这个生成器,形成一批分支数量 / 分支间隔不同的 C 源文件.

命令行参数捕捉

命令行最显而易见的好处在于,你可以通过选项来控制命令的具体行为,比如 cat 会为你呈现文件内容,而 cat -n 可以帮你在文件内容旁边打上行号,那么如何捕捉命令行参数——getopts

while getopts ":b:d:f:s:" opt; do
  case $opt in
    b)
        branch=$OPTARG      # number of switch branches
        ;;
    :)
      echo "Option -$OPTARG requires an argument."
      ;;
    ?)
      echo "Invalid option: -$OPTARG"
      ;;
  esac
done

最简单的元编程

这里的元编程指用代码生成代码,它可以很复杂,然而这里只采取一种最简单的观点——代码,不就是文本文件吗?所以,把代码文本 echo 追加写入到目标文件里去即可:

targetpath="./${dir}/${filename}"

echo -e "/* Created by switch_generator */\n"\
> ${targetpath}     # sleep 0.1 

echo -e \
"int main(){\n\n\
    int i = 0, j = 0;\n\
    switch (i) {\
" >> ${targetpath};  # sleep 0.1

for (( i = 1;i <= $branch; i++ )); do
    record=$i
    i=$(( i*seperate ))
    echo -e \
"    case $i:\n\
        j += $i;\n\
        break;\n\
" >> ${targetpath};  # sleep 0.1
    i=$record
done

echo -e \
"    default:\n\
        j += 1000;\n\
        break;\n\
    }\n\
    return 0;\n\
}" >> ${targetpath} 

源文件流水线

提升抽象的层次,用另一个脚本调用上面的脚本,实现源文件的批量生产,核心代码是:

for (( branch_num = 1; branch_num <= $size; branch_num++ ));do
    filename="${compiler}_branch_${branch_num}"                             
    bash ./switch_generator.sh  -b $branch_num -d $dir -f ${filename}.c 
    # $compiler -S ./${dir}/${filename}.c -o ./${dir}/${filename}.s    
    # ...       
done

把这一堆文件批量编译到汇编,再用 grep 检查是否存在跳转表并报告,我们的任务就完成了,道理差不多,这里不再赘述,如果你好奇讨论题的答案:

  • 当连续分支数量 >= 4(clang) / 5(gcc) 时,编译器将使用跳转表,否则使用 subl, je 条件跳转;
  • 当分支常量间隔 >= 12(clang) / 10(gcc) 时,编译器不再采用跳转表,而是直接用 subl, je 进行条件判断与跳转;
  • 当分支变量为两段连续,但两段之间有较大间隔,如这里的 1,2,…,6, 101,102,…106,gcc 将生成两张跳转表. (此结论来自我的队友 LYT 同学)
Wish You a Nice Day!
Built with Hugo
Theme Stack designed by Jimmy