Linux — Shell 脚本

Bash 版本 #

Dash: Debian Almquist shell

现在 zsh 比较常用,配合 oh-my-zsh 使用更香,zsh 相比 bash 还支持更多脚本功能。

Shell 的父子关系 #

通过 ps -f 可以查看 PIDPPID 号来判断父子关系,创建了子 Shell 就意味着创建了子进程。

  • PID 进程号
  • PPID 父进程号

分号 #

可以在单行中指定要依次运行的一系列命令。这可以通过命令列表来实现,只需将命令之间以分号 ; 分隔即可:

pwd; ls test*; cd /etc; pwd; ls

创建子 shell #

如果要成为进程列表,命令列表必须将命令放入圆括号内。

(pwd; ls test*; cd /etc; pwd; ls)

圆括号的加入使命令列表摇身变成了进程列表,生成了一个子 shell 来执行这些命令。

test ls
gitdemo   makefile  table.csv tarfile   test.rar  unrar.py
test echo $ZSH_SUBSHELL
0
test (ls; echo $ZSH_SUBSHELL)
gitdemo   makefile  table.csv tarfile   test.rar  unrar.py
1

括号还可以继续嵌套。

(pwd; ls test*; cd /etc; (pwd; ls))

后台执行 #

子 Shell 在处理多进程是比较有用,尤其需要在后台运行时:

(sleep 2;pwd; ls test*; cd /etc; pwd; ls; echo $ZSH_SUBSHELL)&

子 Shell 在 sleep 命令睡眠 2 秒钟后执行命令列表,同时通过 & 表示在后台运行。

协程 #

命令 coproc 可以帮助子 Shell 创建并运行协程。

coproc sleep 10

甚至还可以获取后续的协程返回值。

#!/bin/bash

# 使用 coproc 启动一个协程,执行一个简单的命令
coproc MYCOPROC { echo "Hello, World!"; }

# 从协程的输出中读取数据
read -r output <&"${MYCOPROC[0]}"

# 打印输出
echo "The output from coproc is: $output"

# 等待协程结束,并获取其返回值
wait "${MYCOPROC_PID}"
echo "The return value of coproc is: $?"

这里的关键步骤解释如下:

  1. coproc MYCOPROC { command; }:使用 coproc 启动一个名为 MYCOPROC 的协程,执行大括号内的命令。MYCOPROC 变量会被赋值为一个数组,其中 MYCOPROC[0] 是协程的标准输出(STDOUT)的文件描述符,MYCOPROC[1] 是标准输入(STDIN)的文件描述符。
  2. read -r output <&"${MYCOPROC[0]}":使用 read 命令从协程的标准输出中读取数据。<&"${MYCOPROC[0]}" 表示将文件描述符 MYCOPROC[0] 作为输入。
  3. wait "${MYCOPROC_PID}"coproc 启动的协程的 PID 存储在特殊变量 ${MYCOPROC_PID} 中。使用 wait 命令等待协程结束,并通过 $? 获取其返回值。

外部命令和内建命令 #

使用 type 命令可以检查一个命令是否是外部命令或内建命令。外部命令程序通常位于 /bin/usr/bin/sbin/usr/sbin 目录中。

[test] type cd
cd is a shell builtin
[test] type vim
vim is /usr/bin/vim

外部命令 #

外部命令是独立的程序文件,存储在系统的文件系统中。当你执行一个外部命令时,shell 会在系统的 PATH 环境变量定义的目录中查找这个命令的可执行文件。比如 ls(列出目录内容)、grep(文本搜索工具)、awk(文本处理工具)、python(Python 解释器)等。

内建命令 #

内建命令是 shell 的一部分,它们是 shell 程序的一部分,而不是独立的程序。这些命令直接由正在运行的 shell 进程执行,而不需要调用额外的程序文件。比如 cd(改变目录)、echo(打印文本)、history(显示命令历史)、export(设置或显示环境变量)等。

区别 #

可以说,外部命令总是需要创建新的进程来执行,而内建命令可以在当前的 Shell 直接快速执行,这是最明显的区别。

环境变量 #

局部环境变量 #

set 命令既会显示全局和局部环境变量、用户自定义变量以及局部 shell 函数,还会按照字母顺序对结果进行排序。

$ my_var=Hello
echo $my_var
Hello

$ my_var="Hello World"
echo $my_var
Hello World

注意 my_var = "hello world 会报错 zsh: command not found: my_var,所以在 Shell 中的 = 两侧带空格是不对的。另外,按照规范,用户自定义的局部变量用小写字母,系统环境变量用的是大写字母。

全局变量 #

可以在 Shell 中使用 envprintenv 命令来查看全局变量。创建全局环境变量的方法是先创建局部变量,然后再将其导出到全局环境中。

$ my_var=Hello
echo $my_var
export my_var

通过 export 导出后,会生成一个子 Shell,该变量只会在子 Shell 中存在,并不会反映到父 Shell 环境中。

PATH 环境变量 #

持久化环境变量 #

/etc/profile 是 Shell 默认的主启动文件,只要登录 Linux 系统,bash 就会执行 /etc/profile 启动文件中的命令。但这些是用于系统功能,类似的如果要用于用户自己,可以尝试下面的文件:

  • $HOME/.bash_profile
  • $HOME/.bashrc
  • $HOME/.zshrc

创建 Shell 脚本 #

创建 Shell 脚本时,必须在文件的第一行指定要用的 Shell,格式如下:

#!/bin/bash

命令替换 #

命令替换允许将 Shell 命令的输出赋给变量,有以下两种方式。

反引号 #

testing=`date`

$() 格式 #

testing=$(date)

重定向输入和输出 #

输出重定向 #

# 输出内容到文件 file1
date > file1

# 追加输出到 file2
date >> file2

输入重定向 #

# 使用 wc 命令统计文件 file1 中的文本。
wc < file1

# 内联输入重定向
wc << EOF

在命令行使用内联输入重定向时,Shell 会进入次提示符状态并持续显示,直到输入了作为文本标记的那个字符串。wc 命令会统计内联输入重定向提供的数据包含的行数、单词数和字节数。

➜  ~ wc << EOF
heredoc> hello world
heredoc> foo bar
heredoc> 1234
heredoc> EOF
       3       5      25
➜  ~

管道 #

管道帮助你将前一个命令的输出作为下一个命令的输入。虽然重定向也可以实现,只是比较笨拙。

管道的工作原理基于 Unix 哲学中的一个核心概念:“一切皆文件”。在 Unix 和类 Unix 系统中,数据可以通过文件描述符(File Descriptor)来访问。标准输入(stdin)、标准输出(stdout)、和标准错误输出(stderr)分别被分配了文件描述符 0、1 和 2。

当使用管道时,Shell 会创建一个匿名管道(一个内存中的缓冲区),并将 command1 的标准输出连接到这个管道,同时将 command2 的标准输入连接到同一个管道。这样,command1 的输出就直接成为了 command2 的输入。

数学运算 #

命令 expr #

很多数学符号比如 * 在 Shell 另有含义,为了避免表达式误会有两种方式:

# 使用反斜杠
expr $var1 \* $var2

# 使用方括号
var3=$[$var1 * $var2]

浮点数解决方案 bc #

Shell 中的数学计算只支持整数运算,如果要支持浮点数运算还需要借助 Bash 的 bc 计算器。

退出状态码 $? #

Shell 中运行的每个命令都使用退出状态码来告诉shell自己已经运行完毕。退出状态码是一个 0~255 的整数值,在命令结束运行时由其传给 Shell。

你可以获取这个值并在脚本中使用。Linux 提供了专门的变量 $? 来保存最后一个已执行命令的退出状态码。该变量的值会随时变成 Shell 所执行的最后一个命令的退出状态码。

状态码含义
0成功。程序执行成功完成。
1通用错误。通常表示命令行语法错误或命令无法成功执行。
2不适用的 shell 内置命令。
126命令不可执行。通常是因为没有执行权限或不是有效的执行格式。
127命令未找到。尝试执行的命令不存在。
128无效的退出参数。使用了超出 0-255 范围的退出状态。
128+n通过信号 n 终止。这表示程序因为接收到信号 n 而异常终止。例如,128+9 表示因为接收到 SIGKILL (信号 9) 而终止。
130通过 Ctrl+C 终止。等同于 128+2,因为 Ctrl+C 发送 SIGINT (信号 2)。
255*退出状态超出范围。在某些情况下,如果状态码超过 255,有些 shell 可能会报告为 255。

退出状态码的数字含义只是规范,不同的程序和脚本可以自定义使用 1 到 255 之间的任何值来表示特定的错误条件或状态。另外,exist 命令可以在脚本结束时指定一个退出状态码。

if 判断 #

if 语法的例子 #

if-then

if [ 条件 ]; then
    # 条件为真时执行的命令
fi

if-then-else

if [ -e /path/to/file ]; then
    echo "文件存在。"
else
    echo "文件不存在。"
fi

if-then-elif-else

if [ 条件1 ]; then
    # 条件1为真时执行的命令
elif [ 条件2 ]; then
    # 条件2为真时执行的命令
else
    # 上述条件都不为真时执行的命令
fi

if 嵌套

num=10

if [ $num -eq 5 ]; then
    echo "数字等于 5"
elif [ $num -lt 10 ]; then
    echo "数字小于 10"
elif [ $num -le 15 ]; then
    # 在这个 elif 块中嵌套另一个 if
    if (( num % 2 == 0 )); then
        echo "数字小于或等于 15 且为偶数"
    else
        echo "数字小于或等于 15 但不是偶数"
    fi
else
    echo "数字大于 15"
fi

test 命令 #

在 Shell 脚本中,test 命令用于检查某个条件是否成立,并根据检查结果返回退出状态码。退出状态码为 0 表示条件成立(真),非 0 表示条件不成立(假)。test 命令可以用于数值比较、字符串比较、文件属性检查等多种场景。[ ] 是 test 命令的另一种写法,更常见于 if 语句中。

if test $num1 -eq $num2; then
    echo "两个数字相等。"
fi

数值比较 #

  • -eq:等于
  • -ne:不等于
  • -gt:大于
  • -ge:大于等于
  • -lt:小于
  • -le:小于等于
if [ $num1 -eq $num2 ]; then
    echo "两个数字相等。"
fi

字符串比较 #

相等性比较:

  • =:等于
  • !=:不等于
  • -z:字符串长度为零
  • -n:字符串长度非零
str1="hello"
str2="world"

if [ "$str1" = "$str2" ]; then
    echo "两个字符串相等。"
else
    echo "两个字符串不相等。"
fi

如果对字符串用 >< 进行比较,需要加 \ 反斜线进行转义,避免被当成重定向符号使用。注意,比较字符串的时候是按照字母表顺序进行大小比较的,也就是按照 Unicode 编码的值来比较。

if [ $string1 \> $string2 ]; then
    echo "$string1 is greater than $string2."
fi

使用 -n-z 用来测试字符串变量是否为空。其中 -z 操作符用于检查字符串长度是否为零,如果为空则返回真(true)。

if [ -z "$string" ]; then
    echo "String is empty"
fi

-n 操作符用于检查字符串长度是否非零,如果不为空则返回真(true)。

if [ -n "$string" ]; then
    echo "String is not empty"
fi

文件判断 #

  • -e:文件或目录是否存在
  • -f:文件存在且为常规文件
  • -d:文件存在且为目录
  • -r:文件存在且可读
  • -w:文件存在且可写
  • -x:文件存在且可执行
file="/path/to/file"

if [ -e "$file" ]; then
    echo "文件存在。"
else
    echo "文件不存在。"
fi

检查是否可写

file="/path/to/file"

if [ -w "$file" ]; then
    echo "文件可写。"
else
    echo "文件不可写。"
fi

复合条件测试 #

&&|| 用来表示 andor

高级特性 #

使用单括号 #

上面提到过,当使用括号将一行命令包起来后,实际上是创建了一个子 Shell 去执行,然后再返回。在 if 语句中,如果子 Shell 的退出状态码为 0,即为真,执行 then 的部分。

if (command1; command2); then
  echo "Commands succeeded."
else
  echo "Commands failed."
fi

使用双括号 #

双括号用来执行高级的数学表达式。

if ((a > b)); then
  echo "a is greater than b"
fi

很多时候,还是能看到相对应的旧语法,不再推荐使用下面的例子。

var1=5
var2=3
result=$[var1 + var2] # 推荐使用:result=$((var1 + var2))
echo $result

使用双方括号 #

双方括号用来针对高级的字符串比较,是对传统单方括号的增强,这些增强功能和灵活性使得它在 Bash 脚本中被广泛使用,特别是在需要进行模式匹配或正则表达式匹配时。主要包括:

  1. 字符串比较

    使用 ==!= 进行字符串比较。在双方括号内,== 右侧可以使用通配符。

    if [[ $a == $b ]]; then
        echo "a equals b"
    fi
    
    if [[ $a != $b ]]; then
        echo "a is not equal to b"
    fi
    
  2. 模式匹配

    在双方括号中,可以使用 ==!= 进行模式匹配。如果右侧是一个模式(包含通配符如 *?),则左侧的字符串会被检查是否匹配该模式。

    if [[ $filename == *.txt ]]; then
        echo "$filename ends with '.txt'"
    fi
    
  3. 正则表达式匹配

    使用 =~ 进行正则表达式匹配。

    if [[ $string =~ ^[0-9]+$ ]]; then
        echo "$string is a number."
    fi
    
  4. 逻辑运算

    双方括号支持使用 &&|| 进行逻辑与和逻辑或操作,而不需要像单方括号那样分隔多个测试条件。

    if [[ $a == 100 && $b == 200 ]]; then
        echo "Both conditions are true."
    fi
    
  5. 安全性

    双方括号在处理变量时更加安全。即使变量未引用或为空,也不会导致语法错误或意外的行为。

    if [[ $a == $b ]]; then
        echo "a equals b"
    fi
    

注意事项:

  • 在双方括号内,=== 是等价的,都可以用于字符串比较。
  • 使用正则表达式时,正则表达式部分不应被引号包围,否则会被视为普通字符串。
  • 双方括号是 Bash 和一些其他现代 Shell 的特性,不是 POSIX 标准的一部分,因此在非 Bash 环境下可能不可用。

注意事项 ⚠️ #

语法 #

  • 确保在 [ 和 ] 之间留有空格,这是 Shell 语法的要求,因为 [ 实际上是一个命令,而 ] 是它的参数!
  • 使用 (( )) 可以执行算术比较和操作,而使用 [ ] 或 [[ ]] 可以执行字符串和文件比较。
  • 在使用变量时,最好用双引号将变量名括起来,以避免由于变量值中可能包含的空格或特殊字符而导致的错误。

if “零” 会怎样? #

在 Shell 脚本中,条件判断通常依赖于命令的退出状态码。退出状态码为 0 表示命令执行成功(即“真”),非 0 表示命令执行失败或有错误(即“假”)。这与许多编程语言中的布尔逻辑不同,其中 0 通常被解释为“假”,而非零值被解释为“真”。

因此,如果你直接在 if 语句中使用数字作为条件进行判断,应该这样理解:

  • 如果使用数字 0,Shell 会将其解释为成功的退出状态码,即“真”。
  • 如果使用非 0 的数字,Shell 会将其解释为失败的退出状态码,即“假”。

但是,直接在 if 语句中使用数字并不是一个常见的做法。通常,我们会使用命令执行的结果或者使用 test 命令(或 [])来进行条件判断。

下面是一个特殊的例子,直接使用数字作为条件。在这个例子中,exit 0 会导致子 shell 退出并返回状态码 0,表示成功,因此会执行 then 部分的代码,输出“这是真”。

if (exit 0); then
    echo "这是真。"
else
    echo "这是假。"
fi

如果你想通过 test 命令或 [] 来比较数字,应该使用 -eq-ne 等操作符,而不是直接使用数字。

num=0
if [ $num -eq 0 ]; then
    echo "数字等于 0"
else
    echo "数字不等于 0"
fi

在这个例子中,脚本会输出“数字等于 0”,因为我们使用了 -eq 操作符来比较 $num 和 0。

case 语句 #

在 Shell 脚本中,case 语句提供了一种根据模式匹配来执行不同代码块的方法。这在处理多个条件分支时非常有用,可以使代码更加清晰和易于管理。

#!/bin/bash

echo "Enter a number (1-3):"
read number

case $number in
    1)
        echo "You entered one."
        ;;
    2)
        echo "You entered two."
        ;;
    3)
        echo "You entered three."
        ;;
    *)
        echo "Invalid number. Please enter 1, 2, or 3."
        ;;
esac

循环控制 #

for 语句 #

Shell的 for 循环提供了灵活的方式来重复执行命令,无论是遍历一系列的值,处理文件和目录,还是处理命令的输出。

for variable in list
do
    command1
    command2
    ...
done

这里,list可以是一系列值(如数字或字符串),variable是循环中的变量,它会依次取list中的每个值。每次循环时,variable会被设置为list中的当前值,然后执行dodone之间的命令。

for i in 1 2 3 4 5
do
    echo "Number $i"
done

for循环可以与Shell的通配符(如*?)一起使用,以遍历匹配特定模式的文件名。

for file in *.txt
do
    echo "Processing $file"
done

Bash Shell支持C语言风格的for循环语法,这为编写复杂的循环提供了更多的灵活性。

for (( i=1; i<=5; i++ ))
do
    echo "Number $i"
done

在Bash中,你还可以使用花括号{}生成序列,并在for循环中遍历这些序列。

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

或者指定步长:

for i in {0..10..2}  # 从0到10,步长为2
do
    echo "Number $i"
done

还可以遍历一个命令的输出。例如,遍历ls命令列出的文件:

for file in $(ls)
do
    echo "Found file $file"
done

while 语句 #

  1. 基本示例

下面的脚本使用 while 循环打印数字1到5:

#!/bin/bash

counter=1
while [ $counter -le 5 ]
do
    echo "Counter: $counter"
    ((counter++))
done

在这个例子中,只要 counter 的值小于或等于5,循环就会一直执行。每次循环,脚本都会打印当前的 counter 值,然后 counter 的值增加1。

  1. 读取文件

while 循环与 read 命令结合使用,可以逐行读取文件内容:

#!/bin/bash

file="sample.txt"
while IFS= read -r line
do
    echo "$line"
done < "$file"

这个脚本逐行读取 sample.txt 文件中的内容,并打印每一行。IFS=(输入字段分隔符设置为空)和 -r 选项确保每行的内容被准确无误地读取,包括空格和反斜线。

until 语句 #

  1. 基本示例

下面的脚本使用until循环打印数字1到5:

#!/bin/bash

counter=1
until [ $counter -gt 5 ]
do
    echo "Counter: $counter"
    ((counter++))
done

在这个例子中,循环会一直执行,直到 counter 的值大于5。每次循环,脚本都会打印当前的 counter 值,然后 counter 的值增加1。

  1. 读取文件

你也可以使用 until 循环来读取文件中的行。以下是一个示例,它使用 until 循环和 read 命令逐行读取文件内容:

#!/bin/bash

file="sample.txt"
# 打开文件用于读取
exec 3< "$file"

until [ $done ]
do
    # 尝试从文件描述符3中读取一行
    read line <&3 || done=1
    [ $done ] || echo "$line"
done

# 关闭文件描述符3
exec 3<&-

这个脚本使用文件描述符 3 来读取文件 sample.txt 中的每一行。read 命令尝试从文件中读取一行,如果读取失败(比如到达文件末尾),read 命令的退出状态不为 0,done 变量被设置为 1,这导致 until 循环的条件为真,循环结束。

中断控制 #

其他语言常见的 breakcontinue 关键词在 Shell 中同样适用。

重定向循环的输出 #

在 Shell 脚本中,处理循环的输出是一项常见的任务,可以通过多种方式实现,包括但不限于重定向输出到文件、通过管道传递给其他命令处理,或者将输出赋值给变量。下面是一些处理循环输出的常见方法。

  1. 重定向输出到文件

你可以将循环的输出重定向到一个文件中,无论是覆盖文件还是追加到文件末尾。

for i in {1..5}
do
    echo "Line $i"
done > output.txt

这个例子会将循环的输出重定向到 output.txt 文件中,每次执行脚本时都会覆盖原有内容。

for i in {1..5}
do
    echo "Line $i"
done >> output.txt

这个例子会将循环的输出追加到 output.txt 文件末尾,而不是覆盖原有内容。

  1. 通过管道传递给其他命令

你可以通过管道将循环的输出传递给其他命令进行进一步处理。

for i in {1..5}
do
    echo "Line $i"
done | sort -r

这个例子会将循环的输出传递给 sort 命令,并使用 -r 选项进行逆序排序。

  1. 将输出赋值给变量

有时你可能想要将循环的输出赋值给一个变量,以便之后使用。

output=""
for i in {1..5}
do
    output="$output Line $i\n"
done
echo -e $output

这个例子会将每次循环的输出累加到 output 变量中。使用 echo -e 可以正确处理换行符 \n

  1. 使用数组

对于复杂的情况,你可能需要将循环的输出存储到数组中。

output=()
for i in {1..5}
do
    output+=("Line $i")
done

# 打印数组内容
printf "%s\n" "${output[@]}"

这个例子会将每次循环的输出追加到 output 数组中。然后,使用 printf 和数组展开 "${output[@]}" 来打印数组的所有元素。

用户输入 #

脚本参数 $1$2#

Shell 脚本还可以通过命令行参数接收用户输入。这些参数在脚本内部可以通过$1$2等特殊变量访问。

#!/bin/bash

echo "你好,$1。"

运行此脚本时,你可以将用户的名字作为命令行参数传递给脚本:

bash script.sh 张三

如果脚本需要的命令行参数不止 9 个,则仍可以继续加入更多的参数,但是需要稍微修改一下位置变量名。在第 9 个位置变量之后,必须在变量名两侧加上花括号,比如 ${10}

脚本名 $0 #

前面提到的参数是从 $1 开始的,脚本名当然就是 $0 了。但通常在代码中需要使用 basename $0 来使用,basename 命令可以返回不包含路径的脚本名。

#!/bin/bash

name=$(basename $0)
echo "file name is ${name}"

特殊变量 $#$@$* #

特殊变量 $# 都含有脚本运行时携带的命令行参数的个数。那么最后一个参数的应该用 ${$#} 表示,但是花括号内不能使用 $,必须换成 !,即 ${!#} 来表示 Shell 脚本的最后一个参数。

特殊变量 $@$* 用来引用所有的位置参数。其他 $* 会将所有参数视为单个参数,而 $@ 变量会单独处理每一个参数(数组)。

获取用户输入 read 命令 #

在这个例子中,read 命令同时读取用户输入的名字和年龄,并将它们分别赋值给 nameage 变量。

#!/bin/bash

echo "请输入你的名字和年龄:"
read name age
echo "你好, $name, 你$age 岁了."

-p 选项允许直接指定提示符。

#!/bin/bash

read -p "请输入你的名字和年龄:" name age
echo "你好, $name, 你$age 岁了."

还有 -t 选项会指定 read 命令等待输入的秒数。如果计时器超时,则 read 命令会返回非 0 退出状态码。

其他命令 #

shift 命令,可以帮助你跳过不需要的参数,尤其在分离参数和选项的时候非常管用。

getoptgetopts 命令。

输入和输出 #

标准文件描述符 #

在Unix和类Unix操作系统中,文件描述符是一个非负整数,用于表示一个打开的文件、管道或网络连接等。在Shell脚本和程序设计中,文件描述符0、1、2有特殊的含义,它们分别代表标准输入、标准输出和标准错误输出。

文件描述符0:标准输入(stdin) #

  • 文件描述符编号: 0
  • 用途: 用于读取输入。这可以是来自键盘的输入,或者是通过管道或重定向从其他程序或文件中读取的数据。
  • 示例: 在命令行中,< 符号用于将文件重定向到程序的标准输入。

文件描述符1:标准输出(stdout) #

  • 文件描述符编号: 1
  • 用途: 用于输出数据。默认情况下,标准输出会被发送到终端(即屏幕),但也可以通过管道或重定向输出到其他程序或文件。
  • 示例: 在命令行中,> 符号用于将程序的标准输出重定向到文件。

文件描述符2:标准错误输出(stderr) #

  • 文件描述符编号: 2
  • 用途: 用于输出错误信息或诊断信息。默认情况下,标准错误输出也会被发送到终端,但它可以被独立于标准输出重定向到其他地方,这有助于将错误信息与正常输出分开处理。
  • 示例: 在命令行中,2> 符号用于将程序的标准错误输出重定向到文件。

重定向示例 #

  • 将标准输入重定向: command < inputfileinputfile作为command的标准输入。
  • 将标准输出重定向: command > outputfilecommand的标准输出重定向到outputfile
  • 将标准错误输出重定向: command 2> errorfilecommand的标准错误输出重定向到errorfile
  • 同时重定向标准输出和标准错误输出到同一个文件: command > outputfile 2>&1command &> outputfile

永久重定向 exec 命令 #

exec 还可以用来重定向当前shell环境的标准输入、输出和错误输出。例如:

  • 重定向标准输出到文件:
exec > outputfile

之后,所有的标准输出都会被重定向到 outputfile 文件中。

  • 重定向标准输入从文件读取:
exec < inputfile

之后,所有的标准输入都会从 inputfile 文件中读取。

  • 重定向标准错误输出到文件:
exec 2> errorfile

之后,所有的标准错误输出都会被重定向到 errorfile 文件中。

  • 同时重定向标准输出和标准错误输出到同一个文件:
exec > outputfile 2>&1
# 或
exec &> outputfile

系统控制 #

处理信号 #

使用 trap 命令指定 Shell 脚本需要侦测并拦截的 Linux 信号。

trap commands signals

常用的信号对照表有:

信号编号信号名称描述
1SIGHUP挂起信号。当控制终端关闭时,该信号被发送给前台进程组
2SIGINT中断信号。当用户按下中断键(通常是Ctrl+C)时,该信号被发送给前台进程组
3SIGQUIT退出信号。当用户按下退出键(通常是Ctrl+\)时,该信号被发送给前台进程组
9SIGKILL杀死信号。用于立即结束程序的执行
11SIGSEGV段错误信号。当程序访问非法内存时产生
13SIGPIPE管道破裂信号。当进程写入没有读端的管道时产生
14SIGALRM报警信号。由alarm函数产生,用于实现定时提醒
15SIGTERM终止信号。用于请求程序正常退出
17SIGCHLD子进程状态改变信号。当子进程停止或退出时,该信号被发送给父进程
18SIGCONT继续执行信号。用于让停止的进程继续执行
19SIGSTOP停止执行信号。用于停止进程的执行,该信号不能被捕获或忽略
20SIGTSTP终端停止信号。当用户按下停止键(通常是Ctrl+Z)时,该信号被发送给前台进程组
28SIGWINCH窗口大小改变信号。当终端的大小发生变化时,该信号被发送给前台进程组

后台运行 #

在运行的脚本名后面加上 & 会将脚本与当前 Shell 分离开来,并将脚本作为一个独立的后台进程运行。但是此时后台进程仍然可以将标准输出打印到屏幕上,此时可以在命令最开始加上 nohupnohup 命令能阻断发给特定进程的 SIGHUP 信号,当退出终端会话时,可以避免进程退出。

调整优先级 #

可以使用 nice 命令指定脚本优先级,取值为 -20+19 (从最高到最低)。

nice -n 10 ./run.sh > run.out &

优先级取值默认为 0,另外只有 root 用户可以使用这个命令。

指定时间运行 #

参考 at 命令。

定时运行 #

参考 cron 时间表以及 crontab 命令。

脚本函数 #

函数定义 #

Shell 中定义函数有两种。

方式一

greet () {
    echo "Hello, $1!"
}

greet "World"  # 输出: Hello, World!

方式二,使用关键词 function

function function_name {
    # 命令序列
}

函数的参数 #

当然是在函数内部使用特殊变量 $1$2 这种了。

函数的返回值 #

设置函数的返回值还是有两种,一种是在函数执行结束后直接使用退出状态码 $? 来确定。另一种是在函数结尾处使用 return 关键词来实现,但是需要注意:

  • 函数执行一结束就应立刻读取返回值
  • 退出状态码必须介于 0 ~ 255

函数调用 #

Shell 会将函数当做小型脚本来对待,这意味着你可以想普通脚本那样向函数传递参数。

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

result=$(compute 5 3)
echo "The result is: $result"

函数中的变量 #

Shell 脚本中的变量可分为全局变量和局部变量。默认情况下在脚本中定义的任何变量都是全局变量,包括在函数内部,所以和现在的大部分编程语言是有区别的。如何你希望使用局部变量,需要使用 local 关键字。

my_function() {
    local local_var="I am local"
    echo $local_var  # 输出: I am local
}

my_function
echo $local_var  # 不输出任何内容,因为local_var是局部变量

库函数 #

source 命令,也叫点号操作符。

在命令行中使用函数 #

可在 .bashrc 等初始化文件中 source 库函数文件。

图形化 #

核心是 caseselect 命令。

#!/bin/bash

echo "What is your favorite fruit?"
select fruit in Apple Banana Orange Quit
do
    case $fruit in
        Apple)
            echo "Apples are red."
            ;;
        Banana)
            echo "Bananas are yellow."
            ;;
        Orange)
            echo "Oranges are orange."
            ;;
        Quit)
            break
            ;;
        *)
            echo "Invalid option. Please try again."
            ;;
    esac
done

数组 #

Shell 提供了数组功能。

实战 #

备份 #

删除账户 #

监控程序 #

参考 #

Shell 脚本编程现如今没有特别多需要提前了解的细节,毕竟可能每天都在用,参考资料也是非常多,我随便翻了下面几本,基本上大同小异,很多时候都是知道了能做的事情,看得懂语法(不得不说语法是有点别扭),然后在使用的时候去查阅文档确定具体参数。

Linux 命令行与 Shell 脚本编程大全(9.5)

Linux 系统命令及 Shell 脚本实践指南(豆瓣 7.4)

Bash Shell 脚本编程经典实例(8.5)

Linux Shell 核心编程指南(无)

Linux Shell脚本攻略(第3版)(7.8)

跟老男孩学Linux运维:Shell编程实战(7.7)

本文共 9366 字,上次修改于 Apr 10, 2024
相关标签: Linux, Shell