跳转至

Shell脚本

请参考Bash Reference Manual

如果我们想实现比较复杂的操作,写脚本就不可避免了。

例如我们之前在管道里实现的99乘法表:

$ seq 1 9 | awk '{for(j=1;j<=$1;j++) printf "%d×%d=%-2d ", j, $1, j*$1; print ""}'
1×1=1
1×2=2  2×2=4
1×3=3  2×3=6  3×3=9
1×4=4  2×4=8  3×4=12 4×4=16
1×5=5  2×5=10 3×5=15 4×5=20 5×5=25
1×6=6  2×6=12 3×6=18 4×6=24 5×6=30 6×6=36
1×7=7  2×7=14 3×7=21 4×7=28 5×7=35 6×7=42 7×7=49
1×8=8  2×8=16 3×8=24 4×8=32 5×8=40 6×8=48 7×8=56 8×8=64
1×9=9  2×9=18 3×9=27 4×9=36 5×9=45 6×9=54 7×9=63 8×9=72 9×9=81

这里面其实已经用了for循环,只不过由awl帮我们来执行了。

脚本🦶

脚本为什么叫脚本?

script再怎么看也和脚丫子不沾边。

“脚本”之所以叫“脚本”,其实和“脚”没有直接关系,而是中文在翻译时的用词选择。它的核心含义来源于“表演的底本”(如戏剧、影视的剧本),后来被借用到计算机领域。具体可以分两点解释:

  1. 词源来自“表演剧本”
    • “脚本”原指戏剧、影视的台词文本(比如“电影脚本”),强调它是表演的依据,记录动作、对话和流程。
    • 计算机领域的“脚本”借用了这一概念,因为:
      • 脚本代码像剧本一样逐行指导计算机执行任务
      • 脚本通常是轻量、灵活的(类似短剧剧本),而大型程序更像“长篇小说”。
  2. 为什么用“脚”字?
    • 中文的“脚本”是日语“脚本(きゃくほん)”的直接借用(日语原意也是戏剧剧本)。
    • “脚”在日语中有“基础”“底本”的含义(如“脚注”),而“本”指文本,合起来就是“表演的基础文本”。
    • 汉语误以为和“脚”有关,其实只是翻译时的用字巧合,和“脚”本身无关。

想象导演拍电影时依赖剧本(脚本)——程序员写脚本时,计算机就像“演员”,严格按照脚本的“台词”(代码)一步步执行。(如果非要关联“脚”,可以强行联想为“脚本是计算机行动的‘脚印’”,但这属于趣味解读,并非真实来源 😉)

Shebang

一个bash脚本的第一行往往是Shebang:

#!/bin/bash
echo "Hello, World!"

它指明了脚本的解释器,这是unix系统的约定。不仅仅是Shell脚本,Python、Perl等其他语言的脚本也是如此。

除了可以严格指定解释器的路径,还可以通过环境变量来查找:

#!/usr/bin/env bash
echo "Hello, World!"

这里面/usr/bin/env bash就会在环境中寻找bash,根据环境变量的不同来决定使用的程序。

变量

bash脚本中使用=定义变量,注意=的前后不能有空格:

a=2

后续可以用$符号来访问变量:

a=2
echo $a

使用readonly定义只读的变量:

readonly PI=3.14159

除了自定义变量,还可以使用环境变量,它们无需定义由环境提供,例如:

echo $SHELL

所有可用的环境变量可以使用env来查看:

$ env
TERM_PROGRAM=iTerm.app
SHELL=/usr/local/bin/fish
TERM=xterm-256color
TMPDIR=/var/folders/yz/c544wnmx1nz207z6c212d5p00000gn/T/
CONDA_SHLVL=1
GOBIN=/Users/admin/go/bin/
TERM_PROGRAM_VERSION=3.5.14
CONDA_PROMPT_MODIFIER=(base)
GOHOME=/Users/admin/go/
...

数据类型

bash只支持以下几种数据结构:

  • 字符串
    • 根据unix哲学,unix世界几乎所有的东西都是字符串
  • 整数
    • 通常直接定义的变量不被认为是整数,例如a=2会被认为是字符串
    • 需要特别申明:let a=2或者declare -i num=10
  • 数组
    • 数组是多个变量的集合例如:fruits=(apple orange banana)
    • 可以用[]来取数组中的元素,例如echo ${fruits[0]}就会输出apple

引号与扩展

bash中允许使用单引号、双引号和反引号。

双引号允许变量扩展(Expansions,或者叫变量替代),例如:

$ echo "hello $SHELL"
hello /usr/local/bin/fish

但是单引号不允许,只会输出字面量:

$ echo 'hello $SHELL'
hello $SHELL

反引号用来捕获命令的输出,例如:

$ echo "time now:`date`"
time now:2025年 7月22日 星期二 15时48分55秒 CST

使用$()可以实现和反引号相同的效果:

$ echo "time now:$(date)"
time now:2025年 7月22日 星期二 15时49分43秒 CST

当然,捕获的输出不一定要嵌在字符串里,可以赋值给变量:

a=$(ls | wc -l) # 可以使用管道等操作

命令的组合

bash支持多个命令的组合。包括以下几种方式:

  • 顺序执行:command1 ; command2 ; command3
  • 与连接:command1 && command2 && command3
    • 只有前一个执行成功才会执行下一个
  • 或连接:command1 || command2 || command3
    • 只有前一个执行失败才会执行下一个
  • 管道:command1 | command2 | command3
    • 前一个命令的输出作为下一个命令的输入
  • 大括号分组:{ command1; command2; } > output.txt
    • 在当前shell执行,注意{后面和}前面的空格是必要的
  • 小括号分组:(command1; command2) > output.txt
    • 新开一个子shell执行
  • 后台执行:command1 & command2
    • command1将被放在后台

条件语句

bash支持if条件语句:

if [ 条件 ]; then
  # 命令
elif [ 条件 ]; then
  # 命令
else
  # 命令
fi

其中条件的写法有以下几种:

  • 路径判断:-f file-d filefile1 -ef file2
  • 选项判断:-o -v -R -z -n
  • 字符判断:str1 == str2str1 != str2
  • 整数判断:n -eq 2n -gt 2

条件可以用[ ]来承载,也可以用更安全的、好用[[ ]]

特性 [[ ]] [ ] (test)
逻辑运算符 &&, || -a -o
模式匹配 ==, !=(支持通配符,*, ?, [a-z]等) 不支持
正则匹配 =~ 不支持
变量安全 未定义变量不会报错 未加引号可能报错
数值比较 -eq, -lt 等 同 [[ ]]
文件测试 -f, -d 等 同 [[ ]]

除了if条件句,bash还支持case语句:

read -p "输入一个月份 (1-12): " month

case $month in
    12|1|2)
        echo "冬季"
        ;;
    3|4|5)
        echo "春季"
        ;;
    6|7|8)
        echo "夏季"
        ;;
    9|10|11)
        echo "秋季"
        ;;
    *)
        echo "无效月份"
        ;;
esac

上面的语句用read读取了用户的输入,bash也支持select语句读取用户输入:

colors=("红色" "蓝色" "退出")

select color in "${colors[@]}"
do
    case $color in
        "红色")
            echo "你选择了红色"
            ;;
        "蓝色")
            echo "你选择了蓝色"
            ;;
        "退出")
            echo "退出菜单"
            break
            ;;
        *)
            echo "无效选择"
            ;;
    esac
done

脚本运行后,会进入一个交互模式,直到触发break语句退出do循环:

1) 红色
2) 蓝色
3) 退出
#? 2
你选择了蓝色
#? q
无效选择
#? 3
退出菜单

循环语句

bash支持while循环:

count=0
while [ $count -lt 5 ]; do
  echo "Count: $count"
  ((count++))
done

也支持for循环:

for i in {1..5}; do
  echo "Number: $i"
done

{1..5}也是Expansions的一种,详见专门的笔记

还支持until循环(其实就是do循环):

count=1
until [ $count -gt 5 ]#(1)!
do
    echo "Count: $count"
    ((count++))
done
  1. 这里可以不加分号,只需要换一行写do即可

函数

bash支持函数定义:

greet() {
  echo "Hello, $1!"
}
greet "Alice"  # 输出 "Hello, Alice!"

函数中$1$2等表示传入函数的参数,$@或者$*表示所有参数。

默认情况下,函数中定义的变量是全局的,使用local关键字声明局部变量:

test_scope() {
    local var1="local"
    var2="global"
}

test_scope
echo $var1  # 输出为空,因为var1是局部的
echo $var2  # 输出: global

bash的函数无法定义返回值,如需捕获返回值可以使用expansion:

add() {
    echo $(($1 + $2))
}

result=$(add 3 4) # 捕获返回值
echo "3 + 4 = $result"  # 输出: 3 + 4 = 7

脚本参数

脚本的全局变量空间中有如下变量可以使用:

  • $0:脚本名。
  • $1$2等:第1、2个参数。
  • $#:参数个数。
  • $@或者$*:所有参数列表。
  • $?:获取上一条命令的退出状态

如果脚本传入了-短选项或者--长选项,可以用while循环手动处理:

opt.sh
while [[ $# -gt 0 ]]; do
  case "$1" in
    -h|--help)
      echo "SOS"
      shift # 处理下一个参数
      ;;
    *)
      echo "未知参数: $1"
      shift
      ;;
  esac
done

这时候我们调用该脚本就可以传入对应的选项:

$ bash opt.sh -h -a
SOS
未知参数: -a

当然除了手动处理,也可以使用getopts处理:

getopts.sh
while getopts ":ha:b:" opt; do
  case $opt in
    h)
      echo "SOS"
      ;;
    a)
      echo "a is $OPTARG"
      ;;
    b)
      echo "b is $OPTARG"
      ;;
    \?)
      echo "无效选项: -$OPTARG"
      ;;
  esac
done

处理起来更优雅:

$ bash getopts.sh -h -a 2 -c
SOS
a is 2
无效选项: -c

99乘法表

至此我们终于可以输出99乘法表了:

99.sh
print99() {
    for x in {1..9}; do
        for y in {1..9}; do
            if [ $x -ge $y ]; then
                newline=$([ $x -eq $y ] && echo '\n' || echo '  ')
                printf "$y""x$x=$(expr $x \* $y)$newline"
            fi
        done
    done
}
print99
$ bash 99.sh
1x1=1
1x2=2  2x2=4
1x3=3  2x3=6  3x3=9
1x4=4  2x4=8  3x4=12  4x4=16
1x5=5  2x5=10  3x5=15  4x5=20  5x5=25
1x6=6  2x6=12  3x6=18  4x6=24  5x6=30  6x6=36
1x7=7  2x7=14  3x7=21  4x7=28  5x7=35  6x7=42  7x7=49
1x8=8  2x8=16  3x8=24  4x8=32  5x8=40  6x8=48  7x8=56  8x8=64
1x9=9  2x9=18  3x9=27  4x9=36  5x9=45  6x9=54  7x9=63  8x9=72  9x9=81

builtin

或许你已经注意到了,之前用过的很多命令并不是在所有的平台都有。你可以通过type命令查询具体性质:

$ type ls pwd for awk type
ls is an alias for ls -G
pwd is a shell builtin
for is a reserved word
awk is /usr/bin/awk
type is a shell builtin

你可以在bash的手册看看那些命令是内置的。

这里列出所有的bash保留字:

if  then    elif    else    fi  time
for in  until   while   do  done
case    esac    coproc  select  function
{   }   [[  ]]  !

基本上Shell的脚本兼容是个灾难问题。zsh各种花里胡哨的语法、命令和特性在fish上肯定没法完全兼容。所以我们需要尽量使用原生的Shell命令(bash:Bourne Again Shell)。

兼容性最强的Shell?

通常来说,Bash(Bourne Again Shell)被认为是最广泛支持且兼容性最强的选择。它是 Linux 和许多 Unix-like 系统中默认的 shell,支持大量的功能和语法特性,并且兼容原始的 Bourne Shell(/bin/sh)及其他一些经典的 shell。


最后更新: 2025-07-23 01:16:29
创建日期: 2025-01-11 20:28:10

广告

人要恰饭的嘛🤑🤑

评论