Skip to content

第八章 shell编程

shell基本用法

格式要求:首行shebang机制

#!/bin/bash
#!/usr/bin/python

执行方法(注意文件的相对路径):

bash hello.sh
cat hello.sh | bash
bash < hello.sh
chmod +x hello.sh
./hello.sh
~/hello.sh

shell脚本调试:

  • 命令错误继续当前脚本执行。bash -x test.sh会逐行显示每个命令的执行结果,用以排错。
  • 语法错误会中断当前脚本执行。bash -n test.sh会检查语法错误。

变量

常用命名习惯

  1. 变量名:
  2. 使用小写字母,单词之间用下划线分隔(snake_case),这是Shell脚本中最常用的变量命名习惯。
  3. 例如:variable_name="value"

  4. 局部变量名:

  5. 与全局变量类似,通常也使用小写字母和下划线分隔的命名方式。
  6. 如果需要区分局部变量和全局变量,可以在局部变量名前加上下划线,以避免命名冲突。
  7. 例如:local _local_variable="value"

  8. 函数名:

  9. 函数名通常使用小驼峰命名法(lowerCamelCase),首字母小写,后续单词首字母大写。
  10. 例如:doSomething() { ... }

  11. 环境变量:

  12. 环境变量通常使用大写字母,单词之间用下划线分隔,这是操作系统层面的命名习惯。
  13. 例如:export PATH="/usr/bin:/bin"

  14. 数组:

  15. 数组的命名习惯与变量类似,通常使用小写字母和下划线分隔。
  16. 例如:array_name=(element1 element2)

  17. 常量:

  18. 常量通常使用大写字母和下划线分隔,以表示它们是不可变的。
  19. 例如:CONSTANT_NAME="constant_value"

  20. Shell内置变量:

  21. Shell内置变量遵循它们自己的命名习惯,通常是小写字母和下划线分隔,如$HOME$PWD等。

  22. Shell脚本文件名:

  23. 脚本文件名通常使用小写字母,单词之间用下划线分隔,以符合Unix/Linux的文件系统习惯。
  24. 例如:script_name.sh

变量类型

在Shell脚本编程中,变量可以分为几种类型,每种类型都有其特定的用途和引用方式:

  1. 局部变量:
  2. 局部变量只在脚本或函数中定义,并且只在定义它们的脚本或函数中可见。
  3. 引用方式:直接使用变量名,如 var=value
#!/bin/bash
local_var="Hello, World!"
echo $local_var  # 输出: Hello, World!
  1. 环境变量:
  2. 环境变量对所有子进程都可见,包括脚本和程序。
  3. 引用方式:使用 $ 符号,如 echo $PATH
#!/bin/bash
export USER="user1"
echo $USER  # 输出: user1
  1. 位置参数变量:
  2. 位置参数变量用于传递给脚本的命令行参数。
  3. 第一个参数是 $1,第二个是 $2,依此类推,$0 是脚本的名称。
  4. 引用方式:使用 $ 加上参数的位置数字,如 echo $1
#!/bin/bash
echo "First argument: $1"
# 假设脚本以参数 "argument1" 运行: ./script.sh argument1
# 输出: First argument: argument1
  1. 特殊变量:
  2. Shell提供了一些特殊变量,用于存储有关脚本执行的信息。
  3. 例如:$? 表示上一个命令的退出状态码,$$ 表示当前脚本的进程ID。
  4. 引用方式:使用 $ 加上变量名,如 echo $?
#!/bin/bash
echo $?  # 输出上一个命令的退出状态码
  1. 数组变量:
  2. Shell支持一维数组,数组元素通过索引访问。
  3. 引用方式:使用变量名后跟方括号和索引,如 array[0]=value
#!/bin/bash
array=("apple" "banana" "cherry")
echo ${array[1]}  # 输出: banana
  1. 关联数组(在Bash中):
  2. 关联数组允许使用字符串作为索引。
  3. 引用方式:使用变量名后跟方括号和索引,索引可以是字符串,如 declare -A assoc_arrayassoc_array[key]=value
#!/bin/bash
declare -A assoc_array
assoc_array["fruit"]="apple"
assoc_array["vegetable"]="carrot"
echo ${assoc_array["fruit"]}  # 输出: apple
  1. 只读变量:
  2. 使用 readonly 命令可以创建只读变量,它们的值不能被改变。
  3. 引用方式:与普通变量相同,但尝试修改它们会导致错误。
#!/bin/bash
readonly READ_ONLY_VAR="I cannot be changed!"
# 尝试修改会导致错误: READ_ONLY_VAR="new value"
echo $READ_ONLY_VAR  # 输出: I cannot be changed!
  1. 导出变量:
  2. 使用 export 命令可以将变量导出为**环境变量**,使其在子进程中可用。
  3. 引用方式:使用 $ 符号,如 export VAR=value
#!/bin/bash
export VAR="exported value"
# 这个变量可以在子进程中使用
  1. 默认值变量:
  2. 如果变量未定义,可以使用 ${variable:-default_value} 来提供一个默认值。
#!/bin/bash
echo ${variable:-"default value"}  # 如果变量未定义,输出: default value
  1. 间接引用:

    • 使用 ${!variable_name} 可以引用变量名所存储的变量的值。
    #!/bin/bash
    var_name="local_var"
    echo ${!var_name}  # 输出: Hello, World! 假设 local_var="Hello, World!"
    

特殊变量

提示,特殊变量的行为可能会根据不同的Shell实现(如Bash、Zsh、Ksh等)而有所不同。

在Shell脚本中,特殊变量(也称为内置变量或环境变量)是Shell预定义的一些变量,它们具有特定的用途和行为。以下是一些常见的特殊变量:

  1. $0
  2. 脚本的名称。

  3. $nn 是一个数字,通常是1到9):

  4. 位置参数,$1 是第一个参数,$2 是第二个参数,依此类推。

  5. $#

  6. 传递给脚本的位置参数的数量。

  7. $*$@

  8. 所有位置参数的列表。$* 将所有参数视为一个单一的字符串,而 $@ 将每个参数视为列表中的一个元素。

  9. $?

  10. 上一个命令的退出状态码。

  11. $$

  12. 当前Shell进程的进程ID(PID)。

  13. $!

  14. 上一个后台命令的PID。

  15. $-

  16. 显示Shell使用的当前选项,例如 set -x 将输出 x

  17. $_

  18. 上一个命令的最后一个参数。

  19. $?

    • 同上,上一个命令的退出状态码。
  20. $LINENO

    • 当前执行的行号。
  21. $FUNCNAME

    • 当前函数的名称。
  22. $BASH_VERSION$BASH_SOURCE 等:

    • Bash Shell的版本信息和脚本来源信息。
  23. $HOME

    • 用户的主目录。
  24. $PATH

    • 命令搜索路径,由冒号分隔的目录列表。
  25. $PS1

    • 主要提示符,用于显示Shell提示符。
  26. $IFS(Internal Field Separator):

    • 默认情况下用于单词拆分的字符,通常是空格、制表符和换行符。
  27. $OPTIND

    • 选项解析器的当前选项索引。
  28. $SHELL

    • 执行当前Shell脚本的Shell的完整路径。
  29. $USER$LOGNAME

    • 当前用户的名字。

这些特殊变量在Shell脚本中可以用来获取脚本的参数、执行状态、环境信息等。使用这些变量时,不需要使用 $ 来引用它们的值,但如果要设置或修改它们的值,就需要使用 $

例如,打印脚本的名称和参数数量:

#!/bin/bash
echo "Script name: $0"
echo "Number of arguments: $#"

脚本执行

source 命令和 bash 命令在执行Shell脚本时对变量的影响的区别:

  1. source 命令
  2. source 命令用于执行当前Shell环境中的脚本。这意味着脚本中的所有命令都在当前Shell会话中执行,包括变量的赋值。
  3. 使用 source 执行的脚本中的变量和函数定义将影响当前Shell环境,脚本执行完毕后,这些变量和函数仍然存在。
  4. source 命令也可以使用 .(点命令)代替。

使用 source 示例:

source myscript.sh
# 或者
. myscript.sh
  1. bash 命令
  2. bash 命令用于在新的子Shell中执行脚本。这意味着脚本中的命令在一个新的Shell环境中执行,与当前Shell会话隔离。
  3. 使用 bash 执行的脚本中的变量和函数定义不会影响当前Shell环境。脚本执行完毕后,这些变量和函数在当前Shell中不可访问。
  4. 如果需要脚本执行的结果影响当前Shell,可以将输出重定向到 /dev/null 或使用其他方法捕获输出。

使用 bash 示例:

bash myscript.sh

以下是 sourcebash 对脚本变量影响的示例:

#!/bin/bash

# myscript.sh
echo "Before variable assignment: $my_var"
my_var="Hello, World!"
echo "After variable assignment: $my_var"

# 使用 source 执行脚本
echo "Executing with source:"
source ./myscript.sh
echo "Current shell variable: $my_var"  # 变量 my_var 被更新

# 使用 bash 执行脚本
echo "Executing with bash:"
bash ./myscript.sh > /dev/null
echo "Current shell variable: $my_var"  # 变量 my_var 没有被更新

在这个示例中,使用 source 执行 myscript.sh 时,脚本中的变量 my_var 在当前Shell中被更新,因此在脚本执行后仍然可以访问。而使用 bash 执行时,my_var 的更新只限于脚本执行的子Shell中,对当前Shell没有影响。

处理链接文件

在Shell脚本中处理软链接(也称为符号链接,Symbolic Link)和硬链接(Hard Link)时,需要注意以下几点:

  • 当你读取或写入软链接时,实际上是在操作它所指向的原始文件。
  • 当你删除软链接时,只删除了链接本身,不会影响原始文件。
  • 硬链接在脚本中的行为更像原始文件,因为它们共享相同的inode。删除硬链接不会影响原始文件,除非所有硬链接都被删除。
  • 当使用命令如 cpmvrm 时,它们的行为会根据链接的类型而有所不同。例如,cp 默认会复制软链接指向的文件,而不是链接本身,而 cp -P 会复制链接本身作为硬链接。

返回代码

在Shell脚本中,返回代码(Return Code)或退出状态码(Exit Status)用来表示命令或程序执行的最终结果,它们可以被用来决定脚本的下一步操作。

以下是一些关键点:

  1. 返回代码的范围
  2. 通常,返回代码的范围从0到255。
  3. 返回代码0通常表示成功或命令正确执行。
  4. 非零返回代码表示命令执行出错或有异常情况发生。

  5. 获取返回代码

  6. Shell脚本可以通过特殊变量 $? 来获取上一个命令的返回代码。

  7. 使用返回代码进行条件判断

  8. 脚本可以使用 if 语句或 [[ ]] 测试命令来根据返回代码做出决策。

  9. 设置返回代码

  10. 在Shell脚本中,可以通过 exit 命令来设置脚本的返回代码。

  11. 链式命令中的返回代码

  12. 在链式命令中,只有最后一个命令的返回代码会被保留。

  13. 管道中的返回代码

  14. 在管道命令中,整个管道的返回代码是最后一个命令的返回代码。

以下是一些示例:

  • 检查上一个命令的返回代码:
command_that_might_fail
echo $?  # 输出上一个命令的返回代码
  • 根据返回代码执行不同的操作:
if command_that_might_fail; then
    echo "Command succeeded"
else
    echo "Command failed with exit code $?"
fi
  • 设置脚本的返回代码并退出:
exit 0  # 表示成功
exit 1  # 表示失败
  • 使用返回代码进行链式命令的条件判断:
command1 && command2 || echo "command2 failed because command1 did not succeed"
  • 使用返回代码进行管道命令的条件判断:
command1 | command2 || echo "command2 failed"

Shell展开

在Shell脚本中,命令执行的顺序通常遵循从左到右的顺序,但**Shell展开(Shell Expansion)**可以在命令执行前改变字符串的表现形式。Shell展开是Shell在执行命令之前对字符串进行解析和替换的过程。以下是一些常见的Shell展开类型及其执行顺序:

  1. 花括号展开(Brace Expansion)
  2. {} 中的序列会被展开成多个字符串。
  3. 例如:echo {1..3} 展开成 echo 1 2 3

  4. 波浪线展开(Tilde Expansion)

  5. ~ 用于展开用户主目录路径。
  6. 例如:echo ~ 展开成 echo /home/username

  7. 参数展开(Parameter Expansion)

  8. 使用 ${} 进行变量替换。
  9. 例如:var="hello"; echo ${var} 展开成 echo hello

  10. 命令替换(Command Substitution)

  11. 使用反引号 `$() 执行命令,并将输出替换到当前位置。
  12. 例如:echo $(date)echo $(date +%Y)

  13. 算术展开(Arithmetic Expansion)

  14. 使用 $(( )) 执行算术运算。
  15. 例如:echo $(( 3 + 5 ))

  16. 正则表达式匹配(Pattern Matching)

  17. 使用 [[ ]]=~ 进行正则表达式匹配。
  18. 例如:[[ "$str" =~ ^[0-9]+$ ]]

  19. 文件名展开(Filename Expansion)

  20. 使用星号 *、问号 ? 和方括号 [...] 匹配文件名。
  21. 例如:echo *.txt 会列出当前目录下所有以 .txt 结尾的文件。

  22. 引用展开(Quote Removal)

  23. 使用引号 " 来引用字符串,防止Shell展开。
  24. 例如:echo "Hello World"

  25. 空格和换行符展开

  26. 空格和换行符在Shell中通常作为分隔符。

  27. 反斜杠转义(Backslash Escape)

    • 使用 \ 来转义特殊字符或抑制展开。

Shell展开的执行顺序通常是按照上述列表的顺序,从左到右进行。这意味着,例如,花括号展开会在参数展开之前执行。然而,具体的展开顺序可能会受到Shell版本和配置的影响。

示例:

#!/bin/bash

# 假设有以下变量和文件
var="world"
echo "Hello, ${var}"  # 参数展开
echo "Hello, world"  # 引用展开,防止 "world" 被分割成两个单词

# 命令替换
echo "Current date: $(date)"

# 花括号展开
echo "Numbers: {1,2,3}"  # 花括号展开

# 文件名展开
echo "Text files: *.txt"  # 文件名展开,假设当前目录有多个以 .txt 结尾的文件

# 算术展开
echo "Sum: $((3 + 5))"

脚本安全和set

在Shell脚本中,安全性是一个重要的考虑因素,特别是当脚本需要处理外部输入或在多用户环境中运行时。set 命令在Shell脚本中用于设置Shell的行为选项,增强脚本的健壮性和安全性。以下是一些常用的 set 选项及其作用:

  1. set -eset -o errexit
  2. 使脚本在遇到任何错误时立即退出。这意味着如果任何命令返回非零退出状态码,脚本将停止执行。

  3. set -uset -o nounset

  4. 当尝试使用未定义的变量时,脚本将报错并退出。这有助于避免使用未初始化的变量。

  5. set -o pipefail

  6. 在管道命令中,如果任何一个命令失败,整个管道命令将返回失败状态码。默认情况下,只有最后一个命令的退出状态码会被考虑。

  7. set -xset -o xtrace

  8. 打印执行的命令及其参数,用于调试脚本。这可以帮助你了解脚本的执行流程和参数传递情况。

  9. set -n

  10. 不实际执行命令,而是检查脚本的语法。这有助于在运行脚本之前发现潜在的错误。

  11. set -fset -o noglob

  12. 关闭文件名展开(globbing),使得星号 * 和问号 ? 不会被特别处理。

  13. set -C

  14. 允许在脚本中使用嵌入式脚本(通过 source. 命令)。

  15. 组合使用

  16. 可以组合使用多个 set 选项来设置脚本的行为。例如,set -euo pipefail 会同时启用错误立即退出、未定义变量检查和管道命令失败检查。

示例:

#!/bin/bash
# 使用 set 命令增强脚本的安全性

# 组合使用多个选项
set -euo pipefail

# 脚本主体
echo "This is a safe script."

# 尝试使用未定义的变量
echo $undefined_var

在这个示例中,如果尝试使用未定义的变量 $undefined_var,脚本将报错并退出,因为启用了 -u 选项。

请注意,使用 set 命令时应该谨慎,因为某些选项可能会影响脚本的行为,应该根据脚本的具体需求和上下文来选择适当的选项。

例如,set -e 可能会导致脚本在预期的错误发生时退出,而不是处理错误。下面的示例演示了 set -e 可能导致的问题:

#!/bin/bash
# 启用错误立即退出
set -e

# 尝试删除一个可能不存在的文件
rm -f somefile.txt

# 尝试创建一个目录,如果它已经存在,将导致脚本退出
mkdir existing_dir

echo "Script continues to run."

在这个脚本中:

  1. 我们首先使用 set -e 来启用错误立即退出。
  2. 我们尝试删除一个名为 somefile.txt 的文件,即使这个文件不存在,使用 rm -f 命令也会返回成功(因为 -f 选项强制删除并忽略不存在的文件的错误)。
  3. 接下来,我们尝试创建一个名为 existing_dir 的目录。如果这个目录已经存在,mkdir 命令将返回错误(通常是1),由于 set -e 的作用,脚本将在这里停止执行。
  4. 最后,我们打印一条消息,但由于 mkdir 命令失败,这条消息实际上不会被执行。

为了解决这个问题,我们可以:

  • 使用 || 运算符:忽略某些命令的失败。
mkdir existing_dir || true

这样,即使 mkdir 命令失败,|| true 将保证整个表达式返回成功,脚本将继续执行。

  • 使用子Shell:在子Shell中执行可能失败的命令,即使失败也不影响外部脚本的执行。
(mkdir existing_dir)

使用括号创建子Shell执行 mkdir 命令,即使命令失败,主Shell脚本将继续执行。

  • 使用 if 语句:检查命令的返回值,并根据需要决定是否继续执行。
mkdir existing_dir
if [ $? -ne 0 ]; then
    echo "Directory creation failed, but script continues."
fi

这样,即使目录创建失败,脚本也会检查失败并打印消息,然后继续执行。

格式化输出

printf 是一个在Shell脚本中格式化和打印字符串的命令,类似于C语言中的 printf 函数。它提供了一种更可读和灵活的方式来格式化输出,特别是当需要对齐列或格式化数字时。

以下是 printf 的基本用法:

printf "格式字符串" 变量1 变量2 ...
  • 格式字符串:这是定义输出格式的模板,可以包含普通文本和格式化指定符。
  • 变量:这些是将要格式化并插入到格式字符串中的变量。

以下是一些常用的格式化指定符:

  1. %s:字符串。输出参数作为字符串。
printf "Name: %s\n" "John Doe"
  1. %c:字符。输出参数对应的字符(基于其ASCII码)。
printf "Character: %c\n" 65  # 输出 A
  1. %d%i:十进制整数。输出参数作为十进制整数。
printf "Age: %d\n" 30
  1. %u:无符号十进制整数。输出参数作为无符号十进制整数。
printf "Unsigned: %u\n" 4294967295
  1. %f:浮点数。输出参数作为浮点数。
printf "Price: %f\n" 19.99
  1. %e:科学计数法。输出参数作为科学计数法表示的浮点数。
printf "Scientific: %e\n" 123456.789
  1. %E:科学计数法(大写)。与 %e 类似,但使用大写 E。
printf "Scientific (Uppercase): %E\n" 123456.789
  1. %g:通用浮点数。根据数值的大小,选择 %f%e
printf "General: %g\n" 123.456  # 输出 123.456
printf "General: %g\n" 1.23456e+8  # 输出 1.23456e+08
  1. %G:通用浮点数(大写)。与 %g 类似,但使用大写 E。

  2. %x%X:十六进制整数。输出参数作为十六进制整数(小写或大写)。

    printf "Hexadecimal: %x\n" 255  # 输出 ff
    printf "Hexadecimal (Uppercase): %X\n" 255  # 输出 FF
    
  3. %o:八进制整数。输出参数作为八进制整数。

    printf "Octal: %o\n" 255  # 输出 377
    
  4. %b:二进制整数。输出参数作为二进制整数。

    printf "Binary: %b\n" 255  # 输出 11111111
    
  5. %n:不输出任何内容,但是会更新 stdout 的列位置。

    printf "Hello, World"  # 输出 Hello, World 然后光标移动到下一行
    printf "%n"            # 无输出,但光标位置更新为上一行末尾
    
  6. %%:输出百分号字符。

    printf "Percent: %%\n"  # 输出 %
    

除了基本的格式化指定符,printf 还支持宽度、精度和长度修饰符:

  • 宽度:指定输出的最小字符宽度。如果内容较短,则在左侧或右侧填充空格。
printf "Width: %10s\n" "short"  # 输出:Width:     short
  • 精度:对于浮点数,指定小数点后的位数。对于字符串,指定最大字符数。
printf "Price: %.2f\n" 123.4567  # 输出:Price: 123.46
printf "String: %.5s\n" "Hello"    # 输出:String: Hello
  • 长度修饰符:可以指定长整型(lll)或长双精度(L)。
printf "Long integer: %ld\n" 12345678901
printf "Long double: %Lf\n" 12345678901234567890123456789.0

算术和逻辑运算

Shell提供了一系列的算术和逻辑运算符,用于执行数学计算和条件判断。以下是一些基本和高级用法:

算术运算符

  1. +-
a=5
b=3
echo $((a + b))  # 输出 8
echo $((a - b))  # 输出 2
  1. *
a=4
b=2
echo $((a * b))  # 输出 8
  1. /
a=8
b=2
echo $((a / b))  # 输出 4
  1. %
a=7
b=3
echo $((a % b))  # 输出 1
  1. ** 指数(C-style):
echo $((2 ** 3))  # 输出 8
  1. 递增和递减
a=0
echo $((a++))  # 输出 0,然后 a 递增
echo $((++a))  # 输出 2,然后 a 递增
echo $((a--))  # 输出 2,然后 a 递减
echo $(--a)   # 输出 0,然后 a 递减
  1. 赋值运算
a=10
a=$((a + 1))  # a 现在是 11
a=$((a * 2))  # a 现在是 22

逻辑运算符

  1. && 逻辑与:用于条件链,如果第一个命令成功(返回0),则执行第二个命令。
[ condition1 ] && [ condition2 ]
  1. || 逻辑或:如果第一个命令失败(非0返回),则执行第二个命令。
[ condition1 ] || [ condition2 ]
  1. ! 逻辑非:反转上一个命令或条件的返回值。
! [ condition ]

条件表达式

  1. [ condition ][[ condition ]]
  2. 用于测试条件是否为真。
  3. [[ ... ]] 支持更多特性,如模式匹配和正则表达式。
[ $a -eq $b ]  # 检查 a 是否等于 b
[[ $a > $b ]]  # 如果 a 大于 b,则为真
  1. 链式条件
  2. 使用 -a-o 进行逻辑与和或操作。
[ condition1 -a condition2 ]
[ condition1 -o condition2 ]
  1. 文件测试操作符
  2. 用于检查文件的存在性、类型、权限等。
[ -e file ]  # 文件是否存在
[ -f file ]  # 文件是否为普通文件
[ -r file ]  # 文件是否可读
[ -w file ]  # 文件是否可写
[ -x file ]  # 文件是否可执行

高级用法

  1. 算术表达式中的括号
  2. 使用括号来改变运算顺序。
echo $(( (a + b) * (c - d) ))
  1. 使用 let 命令
  2. 另一种执行算术运算的方式。
let "result = (a + b) * c"
  1. 使用 (( )) 进行算术运算和条件表达式
  2. 允许更复杂的表达式和括号内的运算。
if (( (a + b) % c == 0 )); then
    echo "Result is divisible by c"
fi

注意:用$[...]执行算术运算是一种较旧的算术表达式语法,在现代的Shell脚本中,更推荐使用 (( )) 来进行算术运算。

示例:

a=5
b=3

# 使用 $[...] 进行加法运算
result=$[$a + $b]
echo $result  # 输出 8

# 使用 $[...] 进行乘法运算
product=$[$a * $b]
echo $product  # 输出 15

# 使用 $[...] 进行除法运算
quotient=$[ $a / $b ]
echo $quotient  # 输出 1

# 使用 $[...] 进行模运算
remainder=$[ $a % $b ]
echo $remainder  # 输出 2
a=5
b=3

# 使用 (( )) 进行加法运算
((result = a + b))
echo $result  # 输出 8
  1. 使用case语句进行多重条件判断
  2. 类似于其他编程语言的 switch 语句。
case $variable in
    pattern1)
        command1
        ;;
    pattern2)
        command2
        ;;
    *)
        default_command
        ;;
esac
  1. 使用 select 进行简单的菜单驱动脚本
  2. 创建基于文本的菜单。
select option in option1 option2 option3; do
    case $option in
        option1)
            command1
            ;;
        option2)
            command2
            ;;
        *)
            echo "Invalid option"
            ;;
    esac
done

示例-鸡兔同笼

"鸡兔同笼"是一个经典的数学问题,通常用来教授线性方程组的解法。问题是这样的:一笼子里关着鸡和兔子,从上面数,一共有头数 h;从下面数,一共有脚数f。问笼子里各有多少只鸡和兔子?

设鸡的数量为c,兔子的数量为r,则有以下两个方程:

  1. c + r = h(头的总数)
  2. 2c + 4r = f (脚的总数)

可以解这个方程组来找到cr 的值。

下面是一个使用Shell脚本实现的简单例子。

这个脚本没有进行复杂的错误检查,比如检查用户输入是否为数字。

这个脚本首先提示用户输入头和脚的总数,然后使用 bc 命令(一个任意精度的计算器语言)来计算兔子和鸡的数量。最后,脚本输出计算结果,或者如果输入不合理(比如脚的数量小于头的数量的两倍,这意味着会有负数的鸡或兔子),则输出错误信息。

#!/bin/bash

# 读取用户输入的头和脚的数量
read -p "请输入头的数量 (h): " h
read -p "请输入脚的数量 (f): " f

# 计算兔子的数量
rabbits=$(echo "($f - $h * 2) / 2" | bc)

# 计算鸡的数量
chickens=$(echo "$h - $rabbits" | bc)

# 检查结果是否合理(没有负数)
if [ $rabbits -lt 0 ] || [ $chickens -lt 0 ]; then
    echo "输入的数据不合理,无法解决鸡兔同笼问题。"
else
    echo "兔子的数量是: $rabbits"
    echo "鸡的数量是: $chickens"
fi

条件测试

Shell脚本中条件测试的详细说明和示例如下:

数组测试

在Bash中,你可以使用 -a 来检查数组是否非空。

array=(1 2 3)

# 测试数组是否非空
if [ ${#array[@]} -ne 0 ]; then
    echo "Array is not empty"
fi

变量测试

变量测试包括检查变量是否设置(即是否非空)。

var="some value"

# 测试变量是否设置
if [ -n "$var" ]; then
    echo "Variable is set"
fi

# 测试变量是否为空
if [ -z "$var" ]; then
    echo "Variable is empty"
fi

字符串测试

字符串测试包括比较两个字符串是否相等或使用正则表达式匹配。

str1="hello"
str2="hello"

# 测试两个字符串是否相等
if [ "$str1" = "$str2" ]; then
    echo "Strings are equal"
fi

# 使用正则表达式测试
if [[ "$str1" =~ ^[A-Za-z]+$ ]]; then
    echo "String is an alphabetic string"
fi

文件测试

文件测试包括检查文件是否存在、是否可读、是否可写等。

file="example.txt"

# 测试文件是否存在
if [ -e "$file" ]; then
    echo "File exists"
fi

# 测试文件是否可读
if [ -r "$file" ]; then
    echo "File is readable"
fi

# 测试文件是否可写
if [ -w "$file" ]; then
    echo "File is writable"
fi

圆括号 () 和花括号 {} 的用法

圆括号 () 用于算术表达式,花括号 {} 用于命令分组。

# 算术测试
if (( 10 > 5 )); then
    echo "10 is greater than 5"
fi

# 命令分组(分号用于在单个命令行上分隔多个命令,可以看作是一种简单的命令分组)
if true; then
    { echo "This is a group of commands"; echo "executed together"; }
fi

组合测试

使用逻辑运算符 -a(和)和 -o(或)。

# 逻辑与
if [ 10 -gt 5 -a 20 -lt 30 ]; then
    echo "Both conditions are true"
fi

# 逻辑或
if [ 10 -gt 20 -o 20 -lt 30 ]; then
    echo "At least one condition is true"
fi

使用 &&|| 进行复合条件测试。

#!/bin/bash

# 测试文件是否存在并且是可读的
file="testfile.txt"

if [ -e "$file" ] && [ -r "$file" ]; then
    echo "The file exists and is readable."
fi

# 测试文件不存在或者不可写
if [ ! -e "$file" ] || [ ! -w "$file" ]; then
    echo "The file does not exist or is not writable."
fi

使用 test 命令

test 命令是进行条件测试的另一种方式,是 POSIX 兼容的方法,现在使用 [[ ]][ ] 更为方便。

if test 10 -gt 5; then
    echo "10 is greater than 5"
fi

# 字符串长度测试
if test ${#var} -eq 10; then
    echo "Variable has length of 10"
fi

使用 (( )) 进行算术比较

使用 (( )) 可以进行更直观的算术比较。

if (( 10 > 5 )); then
    echo "10 is greater than 5"
fi

使用 [[ ]] 的高级特性

[[ ... ]] 支持模式匹配和其他高级特性。

# 模式匹配
if [[ $var == "pattern*" ]]; then
    echo "Variable matches the pattern"
fi

# 正则表达式匹配
if [[ $var =~ ^[0-9]+$ ]]; then
    echo "Variable is a number"
fi

read用法

read命令用于从标准输入(通常是键盘)读取一行并将其分割成多个变量。

下面是 read 命令的一些常见用法:

read基本用法

# 读取一行输入并存储到变量中
read line
echo "You entered: $line"

读取多个变量

# 一次性读取多个变量
read name age
echo "Name: $name"
echo "Age: $age"

使用默认值

# 如果输入为空,则使用默认值
read -p "Enter your name (default John Doe): " name || read -p "Default name: " name
echo "Name: $name"

带提示的读取

# 带提示的读取
read -p "Enter your name: " name
echo "Hello, $name!"

读取特定数量的字符

# 读取3个字符
read -n 3 char
echo "You entered: $char"

禁止换行符分割

# 读取一行,即使包含空格也不会分割
read -r line
echo "You entered: $line"

读取密码

# 安全地读取密码,不显示输入内容
read -s -p "Enter your password: " password
echo "Password entered."

从文件中读取

# 从文件中读取一行
read -r line < file.txt
echo "First line of the file: $line"

带时间限制的读取

# 设置10秒的超时时间,如果超时则执行后面的命令
read -t 10 -p "Please enter your input within 10 seconds: " input || echo "Timeout!"

读取数组

# 将输入的行分割成数组
read -a array
echo "First element: ${array[0]}"

带选项的读取

# -e 选项允许使用键盘上的 readline 快捷键
# -i 选项显示提示时的默认输入
read -e -i "default_value" input

结合使用 IFSread

# 使用IFS和read来读取并分割字符串
IFS=',' read -ra array <<< "apple,banana,cherry"
echo "First fruit: ${array[0]}"

read重定向

文件中读取
# 从文件的第一行读取数据
read line < file.txt
echo "First line of the file: $line"

这个命令会读取 file.txt 的第一行到变量 line 中。

从文件的特定行读取
# 从文件的第三行读取数据
sed -n 3p file.txt | read line
echo "Third line of the file: $line"

这里使用 sed 来获取文件的第三行,然后通过管道将其传递给 read

从另一个命令的输出读取
# 读取 `ls` 命令的输出
ls | read line
echo "An item from the directory: $line"

这个例子中,read 会读取 ls 命令输出的第一行。

从文件中读取多行
# 读取文件的全部内容到数组
readarray lines < file.txt
echo "First line of the file: ${lines[0]}"

使用 readarray(在Bash中是 read 的别名)可以读取多行数据到一个数组中。

从文件中读取并处理每一行
# 读取文件的每一行并打印
while read line; do
    echo "Processing: $line"
done < file.txt

这里使用了 while 循环和重定向来逐行读取文件。

从命令的输出读取并处理每一行
# 读取 `grep` 命令的输出并处理每一行
grep "pattern" file.txt | while read line; do
    echo "Found match: $line"
done

这个例子中,grep 的输出被逐行读取,并在 while 循环中处理。

读取的时间限制
# 设置10秒的超时时间从用户输入读取
read -t 10 line < /dev/tty
if [ $? -eq 0 ]; then
    echo "You entered: $line"
else
    echo "Timeout occurred!"
fi

在这个例子中,read 尝试从终端读取输入,如果10秒内没有输入,则会超时。

从文件中读取并分割到多个变量

# 假设文件中有两列数据,使用IFS进行分割
IFS=$'\t' # 假设使用制表符分割
read var1 var2 < file.txt
echo "First column: $var1"
echo "Second column: $var2"

这里使用 IFS(Internal Field Separator)来定义字段的分隔符,并从文件中读取两列数据到两个变量中。

bash shell配置文件

按生效范围

Bash Shell的配置文件根据其生效范围可以分为用户级别和系统级别两大类。

用户级别的配置文件

  1. .bashrc
  2. 只在交互式Shell中生效,对当前用户的所有终端会话都有效。
  3. 通常包含别名、shell选项、函数、环境变量等,用于定制用户的交互式Shell环境。

  4. .bash_profile

  5. 在用户登录时生效,无论是通过SSH远程登录还是本地图形界面登录。
  6. 可以设置环境变量、执行脚本、定义用户登录时的一次性任务等。

  7. .bash_login

  8. 如果存在,它将在.bash_profile之前被读取,但只对登录Shell有效。
  9. 通常用于设置与登录Shell相关的环境。

  10. .profile

  11. 在某些系统中,如非Bash默认Shell的系统,可能使用.profile代替.bash_profile.bash_login

  12. .bash_logout

  13. 当交互式Shell会话结束时执行,只对当前用户生效。
  14. 可以执行清理操作,如保存历史记录、清除屏幕等。

系统级别的配置文件

  1. /etc/profile
  2. 系统范围内的配置文件,对所有用户的登录Shell都生效。
  3. 通常用于设置系统级的环境变量、定义系统级的路径等。

  4. /etc/bash.bashrc

  5. 系统范围内的.bashrc文件,对所有用户的交互式Shell都生效。
  6. 可以设置全局别名、shell选项、环境变量等。

  7. /etc/bash.bash_logout

  8. 系统范围内的.bash_logout文件,当所有用户的交互式Shell会话结束时执行。

配置文件的加载逻辑:

  • 当Bash启动一个交互式Shell时,它会按照特定的顺序加载用户的.bashrc文件。
  • 当Bash启动一个登录Shell时,它会首先尝试加载.bash_login,如果该文件不存在,则加载.bash_profile。如果这两个文件都不存在,某些系统可能会尝试加载.profile
  • 系统级的配置文件通常在用户级的配置文件之前加载,以便允许用户级的配置文件覆盖系统级的默认设置。

建议:

  • 避免在用户级的配置文件中设置只影响系统级别的设置,如PATH变量的修改通常放在系统级的配置文件中。
  • 使用条件判断来防止.bashrc在非交互式Shell中被加载,例如使用[ -z "$PS1" ] && return
  • 确保配置文件具有适当的权限,避免安全风险。

按登录方式

从登录方式的角度来看,Bash Shell的配置文件可以分为两类:影响登录Shell的配置文件和影响交互式Shell的配置文件。登录Shell通常是指通过直接登录到系统(如通过SSH或图形界面)所启动的Shell,而交互式Shell则包括登录Shell以及在登录后开启的新的Shell实例。

影响登录Shell的配置文件

  1. .bash_profile
  2. 这是用户登录时加载的配置文件,无论是本地登录还是远程登录。
  3. 它通常用于设置用户的环境变量、函数、别名等,这些设置通常需要在登录时就生效。

  4. .bash_login

  5. 如果存在,此文件将在.bash_profile之前加载,并且仅在登录Shell中生效。
  6. 它用于设置登录时需要的特定环境。

  7. .profile

  8. 在某些系统中,如果.bash_profile.bash_login都不存在,.profile可能会被加载。
  9. 它通常用于设置系统级的环境变量和执行系统级的初始化脚本。

  10. /etc/profile/etc/profile.d/*

  11. 这是系统级的配置文件,适用于所有用户的登录Shell。
  12. /etc/profile 会设置全局的环境变量和路径,而 /etc/profile.d/ 目录下的脚本会按字母顺序加载,用于添加额外的配置。

影响交互式Shell的配置文件

  1. .bashrc
  2. 这是为每个交互式Shell加载的配置文件,无论它是登录Shell还是非登录的交互式Shell。
  3. 它通常包含别名、shell选项、函数等,这些设置用于定制用户的交互式体验。

  4. /etc/bash.bashrc

  5. 这是系统级的.bashrc,对所有用户的交互式Shell都生效。
  6. 它可以设置全局的别名、shell选项等,这些设置会影响到所有用户的交互式Shell环境。

加载顺序和条件判断:

  • Bash Shell在启动时会根据登录方式和Shell类型来决定加载哪些配置文件。
  • .bash_profile 通常会检查是否是交互式登录Shell,并在适当的时候源(source).bashrc 文件,以确保交互式环境的配置也被加载。
  • .bashrc 文件通常也会包含条件判断,以确保它不会在非交互式Shell中被重复加载。

下面的示例中,Bash Shell对登录方式进行了判断,以确保用户的环境在不同的登录方式和Shell类型中都能得到适当的配置。

# ~/.bash_profile
if [ -f ~/.bashrc ]; then
   source ~/.bashrc  # 加载交互式环境的配置
fi

# ~/.bashrc
if [ -z "$PS1" ]; then
    return  # 非交互式Shell,不加载.bashrc
fi

按功能分类

Bash Shell的配置文件按功能分类通常可以分为两类:profile类和bashrc类。

Profile 类配置文件

这类配置文件主要影响登录Shell环境。它们在用户登录时被读取和执行,无论是本地登录还是远程登录(如通过SSH)。Profile文件用于设置用户的环境变量、函数、别名等,通常只执行一次。

  1. .bash_profile
  2. 用户的主配置文件,每个用户都可以有自己的.bash_profile文件。
  3. 通常在用户的主目录下,用于设置个人的环境配置。

  4. .bash_login

  5. 如果存在,这个文件将在.bash_profile之前被读取。
  6. 较少使用,但在某些系统中可能用于特定的登录设置。

  7. .profile

  8. 对于非Bash Shell,如Sh或Ksh,可能使用.profile作为主配置文件。
  9. 如果Bash作为登录Shell,且不存在.bash_profile.bash_login.profile将被读取。

  10. /etc/profile

  11. 系统级的profile文件,影响所有用户的登录Shell。
  12. 用于设置系统环境变量、系统级函数和执行系统级初始化。

  13. /etc/profile.d/

  14. 这个目录包含多个脚本文件,它们按字母顺序被/etc/profile逐一source。
  15. 用于组织系统级配置,使得维护更加方便。

Bashrc 类配置文件

这类配置文件主要影响交互式Bash Shell环境。它们在每个新的交互式Shell启动时被读取和执行,无论这是登录Shell还是用户手动启动的新Shell。

  1. .bashrc
  2. 用户的交互式Shell配置文件,每个用户都可以有自己的.bashrc文件。
  3. 包含别名、shell选项、环境变量、函数等,用于定制交互式体验。

  4. /etc/bash.bashrc

  5. 系统级的bashrc文件,影响所有用户的交互式Shell。
  6. 可以设置全局别名、shell选项等,这些设置将应用于所有用户的交互式Shell。

Bash Shell在启动时会根据Shell的类型(登录Shell或交互式Shell)来决定加载哪些配置文件:

  • 登录Shell:首先检查.bash_login,然后是.bash_profile,最后是.profile(如果前两者都不存在)。
  • 交互式Shell:加载.bashrc.bash_profile中通常会包含source .bashrc的命令,以确保交互式环境的配置也被加载。

例如,.bash_profile 可能会包含以下内容来source .bashrc

if [ -f ~/.bashrc ]; then
   source ~/.bashrc
fi

这种条件判断确保了.bashrc只在交互式Shell中被加载,而不会在脚本执行或非交互式Shell中被加载。

使bash shell配置生效

通常需要执行以下步骤使Bash Shell配置文件生效:

  1. 编辑配置文件: 打开或创建相应的Bash配置文件,如.bashrc.bash_profile等,并进行所需的更改。

  2. 保存更改: 保存对配置文件的更改。

  3. 应用配置

  4. 对于用户级别的配置文件(如.bashrc.bash_profile),可以通过启动一个新的Shell或使用以下命令来应用更改:

    source ~/.bashrc
    

    或者使用.命令(点命令):

    . ~/.bashrc
    
  5. 对于系统级的配置文件(如/etc/bash.bashrc/etc/profile),通常需要所有用户重新登录或重新启动系统来生效。

  6. 检查配置文件是否被加载

  7. 要检查配置文件是否被加载,可以在Shell中使用echo命令输出配置文件中设置的变量或别名的状态:

    echo $YOUR_VARIABLE
    
  8. 如果变量或别名没有按预期输出,可能需要检查配置文件是否正确设置或是否在正确的位置。

  9. 重新登录或重启终端

  10. 对于影响登录Shell的配置文件,如.bash_profile,通常需要用户重新登录以使更改生效。
  11. 对于交互式Shell的配置文件,如.bashrc,通常需要重新启动终端或使用source命令。

  12. 检查环境变量

  13. 如果你更改了环境变量,可以使用env命令或echo $VARIABLE_NAME来检查它们是否被正确设置。

  14. 使用脚本自动化

  15. 如果你需要在多个系统或多个用户上应用配置,可以编写脚本来自动化编辑和应用配置文件的过程。

  16. 考虑配置文件的加载顺序

  17. 理解不同配置文件的加载顺序和它们之间的依赖关系,以确保配置正确应用。

  18. 检查权限

  19. 确保配置文件具有正确的文件权限,以便当前用户可以读取和修改它们。

  20. 查看Shell启动文件

    • 检查/etc/passwd文件中的用户Shell设置,确保Bash是默认Shell。

流程控制

条件选择

Shell中条件选择主要通过 if 语句和 case 语句来实现。

if 语句

if 语句用于基于条件的判断来执行不同的命令块。

基本语法:

if [ condition ]
then
    # Commands to execute if condition is true
elif [ another_condition ]
then
    # Commands to execute if the first condition is false and another_condition is true
else
    # Commands to execute if both conditions are false
fi

示例:

num=10

if [ $num -gt 5 ]
then
    echo "Number is greater than 5"
elif [ $num -lt 5 ]
then
    echo "Number is less than 5"
else
    echo "Number is equal to 5"
fi

case 语句

case 语句是一种多路选择结构,它允许根据不同的模式匹配来执行不同的命令块。

基本语法:

case variable in
    pattern1)
        # Commands to execute if variable matches pattern1
        ;;
    pattern2)
        # Commands to execute if variable matches pattern2
        ;;
    *)
        # Default commands to execute if no patterns match
        ;;
esac

示例:

echo "Enter a number (1, 2, or 3):"
read num

case $num in
    1)
        echo "You entered one"
        ;;
    2)
        echo "You entered two"
        ;;
    3)
        echo "You entered three"
        ;;
    *)
        echo "You did not enter a valid number"
        ;;
esac

条件选择表达式

ifcase 语句中,可以使用各种条件表达式来检查文件状态、字符串比较、数值比较等。

  • 数值比较
[ $a -eq $b ]   # 等于
[ $a -ne $b ]   # 不等于
[ $a -gt $b ]   # 大于
[ $a -ge $b ]   # 大于等于
[ $a -lt $b ]   # 小于
[ $a -le $b ]   # 小于等于
  • 字符串比较
[ "$a" = "$b" ]   # 字符串相等
[ "$a" != "$b" ]  # 字符串不等
  • 文件测试
[ -e "file" ]     # 文件存在
[ -f "file" ]     # 文件是普通文件
[ -d "file" ]     # 文件是目录
[ -r "file" ]     # 文件可读
[ -w "file" ]     # 文件可写
[ -x "file" ]     # 文件可执行
  • 逻辑运算符
! [ condition ]   # 逻辑非
[ condition1 ] && [ condition2 ]   # 逻辑与
[ condition1 ] || [ condition2 ]   # 逻辑或

注意事项:

  • 条件表达式需要放在方括号 [[ ... ]] 中,或者使用 test 命令。
  • 在使用 if 语句时,确保每个条件后面都有 then 关键字,每个命令块结束后都有 fielse/elif
  • 在使用 case 语句时,每个模式分支后都应该有双分号 ;; 来表示分支结束。

循环

Shell脚本提供了几种循环结构,允许你重复执行一段代码直到满足特定条件。以下是Shell中常见的循环结构:

for 循环

for 循环用于遍历列表中的每个元素并执行一系列命令。

基本语法:

for variable in list
do
    # Commands to execute for each item
done

示例:

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

while 循环

while 循环会在给定的条件为真时不断执行一段代码。

基本语法:

while condition
do
    # Commands to execute
done

示例:

i=1
while [ $i -le 5 ]
do
    echo "Welcome $i times"
    ((i++))
done

until 循环

until 循环与 while 循环相反,它会在给定的条件为假时不断执行一段代码。

基本语法:

until condition
do
    # Commands to execute
done

示例:

i=1
until [ $i -gt 5 ]
do
    echo "Welcome $i times"
    ((i++))
done

case语句

case语句是一种选择结构,它允许你根据不同的情况执行不同的代码块。

基本语法:

case variable in
    pattern1)
        # Commands to execute if variable matches pattern1
        ;;
    pattern2)
        # Commands to execute if variable matches pattern2
        ;;
    *)
        # Default commands to execute if no patterns match
        ;;
esac

示例:

case $1 in
    start)
        echo "Starting process..."
        ;;
    stop)
        echo "Stopping process..."
        ;;
    restart)
        echo "Restarting process..."
        ;;
    *)
        echo "Unknown command: $1"
        ;;
esac

select 循环

select 循环用于创建简单的菜单驱动脚本,让用户从列表中选择一个选项。

基本语法:

select variable in list
do
    # Commands to execute for each option
    echo "Selected option: $variable"
done

示例:

echo "Select an option:"
select option in start stop restart
do
    echo "You selected $option"
    case $option in
        start)
            echo "Starting process..."
            ;;
        stop)
            echo "Stopping process..."
            ;;
        restart)
            echo "Restarting process..."
            ;;
    esac
    break
done

循环控制语句

Shell控制循环执行的语句:

  • break:立即退出循环。
  • continue:跳过当前循环的剩余部分,并继续执行下一次循环迭代。

示例:使用 breakcontinue

for i in {1..10}
do
    if [ $i -eq 6 ]; then
        break
    fi
    echo "Number is $i"
done

for i in {1..10}
do
    if [ $i -eq 3 ]; then
        continue
    fi
    echo "Number is $i"
done

函数

Shell脚本中的函数是封装一系列命令的代码块,可以提高脚本的可读性和可维护性。

函数的组成

一个基本的Shell函数由以下部分组成:

  • 函数名:用于调用函数的标识符。
  • 函数体:大括号{}内定义的一系列命令。
  • 参数列表:函数可以有零个或多个参数,这些参数在函数体内作为变量使用。

示例:

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

查看shell函数

列出所有函数

  1. 使用 declaretypeset 命令: 这些命令可以用来显示所有变量和函数,包括它们的属性。
declare -F
# 或者
typeset -F

这将列出所有函数及其简短的描述。

  1. 使用 compgen 命令compgen 命令可以用于列出所有用户可补全的元素,包括函数。
compgen -A function

查看特定函数的定义

  1. 使用 type 命令type 命令可以显示函数或别名的名称和定义。
type name_of_function

如果函数名为 my_function,使用 type my_function 可以显示其定义。

  1. 查看脚本文件: 如果函数定义在外部脚本文件中,你可以直接查看该文件的内容。
cat /path/to/script.sh
  1. 使用 grep 命令grep 可以用来搜索包含特定函数名的行。
grep 'my_function\(\)' /path/to/script.sh
  1. 使用 sedawk: 这些文本处理工具可以用来提取包含函数定义的文本块。
sed -n '/^my_function /,/^}/p' /path/to/script.sh
# 或者
awk '/^my_function /,/^}/ { print }' /path/to/script.sh

检查函数是否存在

  1. 使用 type 命令type 命令还可以检查函数是否存在。
if type my_function &> /dev/null; then
    echo "Function exists"
else
    echo "Function does not exist"
fi
  1. 使用 -x 选项: 尝试执行带有 -x 选项的函数名,如果函数存在,-x 将使其执行。
if ! my_function -x &> /dev/null; then
    echo "Function does not exist or is not executable"
fi

删除函数

在Shell中,删除函数通常意味着移除一个已经定义的函数。删除函数并不是Shell内置的直接功能,需要通过一些方法来实现。

  1. 使用 unset 命令unset 命令通常用于删除变量,但也可以用来删除函数。要删除一个函数,可以使用 -f 选项。
unset -f my_function

这将删除名为 my_function 的函数。

  1. 使用Shell特性: 在某些Shell(如bash)中,如果一个函数体内没有任何命令,它将不会执行任何操作,这可以视为一种“删除”函数的行为。
my_function() {
}

这样定义的函数实际上不会对任何输入参数产生响应。

  1. 使用条件判断: 在函数内部使用条件判断来决定是否执行函数体。如果条件不满足,函数将不执行任何命令。
my_function() {
    if false; then
        # 原来的函数体
    fi
}
  1. 使用 typesetdeclare: 在bash中,使用 typesetdeclare 并结合 -f 选项可以查看函数,但并不能删除函数。

删除函数是一个不常见的操作,因为一旦脚本或命令行环境被初始化,函数通常会保持不变。然而,在某些复杂的脚本中,可能需要动态地修改或删除函数定义。使用 unset -f 是实现这一目的的标准方法。

补充:

在Shell中,别名(alias)和函数(function)可能具有相同的名称,但它们的优先级是不同的。当一个命令被执行时,Shell 会按照以下顺序进行解析:

  • Shell 内置命令:如果命令是Shell的内置命令,它将首先被执行。
  • 函数:如果存在同名的函数,Shell 将执行该函数。
  • 别名:如果函数不存在,Shell 将检查是否存在同名的别名,并执行别名定义的命令。
  • 外部命令:如果上述都没有匹配,Shell 将按路径在系统上搜索可执行文件。

函数的调用

函数可以通过其名称和参数列表进行调用:

  • 交互式调用:在Shell提示符下直接调用。
  • 非交互式调用:在脚本中或其他函数内部调用。
  • 函数文件调用:在外部脚本文件中定义的函数,通过source命令或包含在配置文件中加载。

示例:

# 交互式调用
my_function "John"

# 非交互式调用
result=$(my_function "John")

# 函数文件调用
source /path/to/function_script.sh
my_function "John"

函数返回值

Shell函数可以通过echo命令输出数据,并使用$?获取上一个命令的退出状态码。此外,可以使用命令替换来捕获函数的输出:

  • 返回退出状态码return命令(仅限数值)。
  • 返回输出:使用$()或反引号。

示例:

my_function() {
    echo "Hello"
    return 0
}

# 使用返回值
status=$?
if [ $status -eq 0 ]; then
    echo "Function executed successfully"
fi

# 捕获输出
output=$(my_function)
echo "Function output: $output"

环境函数

在Shell脚本中,环境函数通常指的是在父进程中定义,并且能够在子进程中使用的函数。这通常是通过在父进程的配置文件(如 .bashrc.profile)中定义函数,然后这些函数在子进程的Shell会话中被自动加载来实现的。

以下是一个示例,演示如何在父进程中定义一个函数,并使其在子进程中也可用:

  1. 编辑用户的Shell配置文件,例如 .bashrc,在其中定义一个函数:
# ~/.bashrc 文件的末尾添加以下内容

# 定义一个简单的环境函数
env_function() {
    echo "This is an environment function."
}

# 使修改立即生效
source ~/.bashrc
  1. 保存文件并关闭编辑器。

  2. 打开一个新的终端会话或子Shell,函数应该已经定义好了,并可以被调用:

env_function
  1. 这将输出:
This is an environment function.

这说明函数 env_function 在新的子Shell中被成功加载并执行。

跨会话的函数加载

为了让函数在所有新的Shell会话中自动加载,需要确保 .bashrc 或其他配置文件被正确地source。大多数现代Shell在启动时会自动加载 .bashrc 文件,但可以通过以下命令手动source:

source ~/.bashrc
# 或者使用 dot 命令
. ~/.bashrc

函数与子Shell的关系

  • 子Shell:当你开启一个新的终端窗口或标签时,通常会启动一个新的子Shell。
  • 环境函数:定义在父Shell的配置文件中的函数,通过配置文件的加载,可以在所有子Shell中使用。

提示:

  • 确保函数定义没有语法错误,否则在source配置文件时可能会导致错误。
  • 某些环境(例如非交互式Shell或某些脚本环境)可能不会加载 .bashrc,而是加载其他配置文件,如 .bash_profile.profile
  • 如果函数需要在脚本中使用,可以直接在脚本文件中定义,或者通过source命令加载包含函数定义的文件。

函数参数

在Shell脚本中,函数的参数是传递给函数的值,这些值可以用于控制函数的行为。以下是关于Shell函数参数的详细介绍:

参数的基本使用

在Shell函数中,参数通过位置标识,如 $1, $2, $3, ...,其中 $1 是第一个参数,$2 是第二个参数,依此类推。

my_function() {
    echo "第一个参数是: $1"
    echo "第二个参数是: $2"
    # ... 以此类推
}

# 调用函数
my_function "value1" "value2"

特殊参数变量

Shell提供了一些特殊的变量来处理函数参数:

  • $#:参数的数量。
  • $*$@:所有参数的列表。两者在大多数情况下可以互换使用,但 $@ 在双引号中会保留参数的空格和特殊字符。
  • $0:函数的名称(在函数内部,它仍然是脚本本身的名称)。
my_function() {
    echo "参数个数: $#"
    echo "所有参数: $*"
    echo "所有参数(保留空格): $@"
}

my_function "hello" "world script"

参数默认值

在Shell中,你可以为函数参数设置默认值,当调用函数时没有提供某个参数时使用。

my_function() {
    local param1=${1:-default1}
    local param2=${2:-default2}
    echo "参数1: $param1"
    echo "参数2: $param2"
}

my_function "custom1"

这将输出:

参数1: custom1
参数2: default2

参数展开

参数可以通过多种方式展开:

  • ${variable:-word}:如果 variable 未设置或为空,则使用 word 作为默认值。
  • ${variable:=word}:如果 variable 未设置或为空,则设置 variableword
  • ${variable:+word}:如果 variable 设置且不为空,则使用 word
  • ${variable:?message}:如果 variable 未设置或为空,则打印 message 并退出脚本。

参数的数组处理

在处理数组参数时,可以使用 "$@" 来保持数组的元素作为独立的参数传递给函数。

my_function() {
    echo "数组参数:${@:2:3}"  # 从第二个元素开始的三个元素
}

my_function "one" "two" "three" "four"

这将输出:

数组参数:three four

参数的间接性

你可以使用变量来存储参数的索引,并在函数中引用这些变量。

index=1
my_function() {
    echo "通过索引传递的参数: ${!1}"
}

my_function "$index"

这将输出:

通过索引传递的参数: two

命名参数(具名参数)

虽然Shell函数本身不支持命名参数(具名参数),但你可以使用变量名来模拟这种行为。

my_function() {
    echo "名字: $name"
    echo "年龄: $age"
}

name="John"
age=30
my_function "$name" "$age"

提示:

  • 确保在需要时对参数进行检查,以避免未定义变量的错误。
  • 使用双引号来确保参数被正确地作为字符串处理,特别是当参数可能包含空格或特殊字符时。

函数变量

以下是Shell中变量按作用域的划分:

  1. 全局变量
  2. 全局变量在脚本的任何位置定义后,在该脚本的所有函数和命令中都可见。
GLOBAL_VAR="value"
my_function() {
    echo $GLOBAL_VAR
}
  1. 局部变量
  2. 局部变量仅在定义它们的函数或代码块内部可见。使用 local 关键字定义局部变量。
my_function() {
    local local_var="value"
    echo $local_var
}
  1. 环境变量
  2. 环境变量是全局可见的,它们在脚本、子进程、以及任何由脚本启动的程序中都可见。使用 export 关键字将变量标记为环境变量。
export ENV_VAR="value"
  1. 位置参数变量
  2. 位置参数变量 $1, $2, $3, ... 用于传递给脚本的命令行参数。它们在脚本的整个作用域内都是可见的。
echo "First argument: $1"
  1. 特殊变量
  2. Shell提供了一些特殊变量,如 $0(脚本名称)、$#(参数数量)、$*$@(所有参数的列表)等。这些变量在脚本的整个作用域内都是可见的。

  3. 读取命令的变量

  4. 使用 read 命令读取的变量在读取它们的上下文中是局部的,除非使用 export 显式地将它们声明为环境变量。
read name
echo "Name: $name"
  1. declaretypeset 创建的变量:
  2. 使用 declaretypeset 创建的变量是全局的,除非使用 local 声明为局部变量。
declare my_var="value"
  1. 别名
  2. 别名在它们被定义的地方是全局的,但它们的作用类似于快捷方式,并不存储实际的值。
alias ll='ls -l'
  1. 函数返回值
  2. 虽然Shell函数不能直接返回值,但可以通过打印输出并捕获(如使用命令替换 $(...)),或者通过修改全局变量来传递“返回值”。
my_function() {
    local result="value"
    echo $result
}
result=$(my_function)

递归函数

在Shell脚本中,递归函数是一种可以调用自身的函数。允许函数解决那些可以被分解为相似子问题的问题。

下面是对Shell中递归函数和一些高级用法的详细介绍:

基本递归函数示例

# 计算阶乘的递归函数
factorial() {
    local n=$1
    if [[ $n -le 1 ]]; then
        echo 1
    else
        local prev=$(factorial $((n - 1)))
        echo $((n * prev))
    fi
}

echo $(factorial 5)  # 输出: 120

提示:

  • 递归深度:Shell脚本的递归深度是有限的,通常在1000到2000次调用之间,具体取决于系统和Shell的配置。过深的递归可能导致栈溢出。
  • 性能问题:递归可能不是执行某些任务的最高效方式,特别是当递归层数较深时。在可能的情况下,考虑使用循环结构。

除了基本的递归,Shell函数还支持一些高级用法:

  1. 函数作为参数传递: 可以将函数作为参数传递给其他函数,或在函数内部定义匿名函数。
apply_function() {
    local func=$1
    $func
}

my_function() {
    echo "Hello, World!"
}

apply_function my_function
  1. 函数返回函数: 一个函数可以返回另一个函数,从而实现更复杂的逻辑。
make_function() {
    echo 'my_function() { echo "Dynamic function"; }'
}

apply_function=$(make_function)
eval "$apply_function"
my_function
  1. 函数的动态创建: 在Shell中,可以动态创建函数,这在某些高级脚本中非常有用。
create_function() {
    eval "$1() { echo Dynamically created function; }"
}

create_function my_dynamic_function
my_dynamic_function
  1. 使用 source. 命令加载函数定义: 可以在一个文件中定义多个函数,然后通过 source 命令在另一个脚本或Shell会话中加载它们。
# functions.sh 文件
function_a() { echo "Function A"; }
function_b() { echo "Function B"; }

# 另一个脚本或Shell会话
source functions.sh
function_a
function_b
  1. 函数的陷阱处理: 在函数中使用 trap 命令可以捕获信号或退出状态,实现复杂的错误处理和清理逻辑。
my_function() {
    trap 'echo "Function interrupted"; return 1' SIGINT
    echo "Processing..."
    # 执行一些操作...
}
  1. 函数的多返回值: 虽然Shell函数不能直接返回多个值,但可以通过输出格式化字符串,然后调用者使用 read 命令解析这些值。
my_function() {
    echo "value1 value2"
}

value1= value2=
read value1 value2 <<< $(my_function)

其它脚本工具

信号捕捉trap

在Shell脚本中,trap 命令用于捕捉和处理信号或某些特定事件。信号是Linux和Unix系统中用于进程间通信的机制。使用 trap,你可以定义当脚本接收到某个信号时应该执行的代码块。

基本语法:

trap 'response_command' signal
  • response_command:当信号被触发时,Shell将执行的命令或命令列表。
  • signal:要捕捉的信号名称或编号。

常见的信号:

  • SIGINT:通常由用户通过Ctrl+C触发。
  • SIGTERM:终止信号,可以由 kill 命令发送。
  • SIGHUP:通常在用户退出终端时发送。
  • SIGQUIT:通常由用户通过Ctrl+\触发。
  • SIGKILL:不能被捕捉、阻塞或忽略的强制终止信号。

示例:

  1. 捕捉SIGINT信号
trap 'echo "捕捉到 SIGINT,退出脚本。"; exit 1' SIGINT
  1. 捕捉多个信号
trap 'echo "捕捉到信号,退出脚本。"; exit 1' SIGINT SIGTERM
  1. 使用函数作为响应
exit_script() {
    echo "正在退出脚本..."
    # 执行清理工作
    exit 1
}

trap exit_script SIGINT SIGTERM
  1. 忽略信号
trap '' SIGINT  # 忽略SIGINT信号
  1. 清理退出
cleanup() {
    echo "执行清理工作..."
    # 清理资源,如删除临时文件
}

trap cleanup EXIT

在这个例子中,cleanup 函数将在脚本退出时执行,无论退出是由信号、错误或正常退出引起的。

  1. 使用 trap 捕获退出
trap 'echo "脚本正在退出。"' EXIT

使用 EXIT 作为信号可以确保在脚本退出时执行一些清理工作。

提示:

  • trap 命令对子Shell有效,因此在父Shell中设置的信号处理不会影响子Shell。
  • 某些信号,如 SIGKILLSIGSTOP,不能被 trap 捕捉。
  • 不当的信号处理可能会导致脚本行为不可预测。

创建临时文件mktemp

mktemp 是一个在Unix和类Unix系统中广泛使用的命令行工具,用于创建临时文件或目录。这个命令特别有用,因为它能够生成具有唯一名称的文件或目录,从而避免命名冲突。

基本用法:

mktemp

如果不带任何参数调用 mktemp,默认情况下,它会在 /tmp 目录下创建一个临时文件,并打印新创建的文件名到标准输出。

创建临时目录:

要创建一个临时目录,可以使用 -d 选项:

mktemp -d

这将在 /tmp 目录下创建一个临时目录,并将目录的路径打印到标准输出。

指定前缀:

可以使用 -p 选项后跟一个前缀来指定临时文件或目录的名称前缀:

mktemp -p myapp

这将创建一个名称以 myapp 开头的临时文件。

指定模板:

使用 -t 选项可以指定一个模板,mktemp 将使用这个模板来创建文件或目录。模板通常包含一个 X,它将被替换为一个随机字符:

mktemp -t mytemp.XXXXXX

这将创建一个名为 mytemp 开头,后面跟着随机字符的临时文件。

指定目录:

可以使用 --tmpdir 选项来指定一个不同的目录来存放临时文件或目录:

mktemp --tmpdir=tempfiles

这将在 tempfiles 目录下创建临时文件。

安全特性:

mktemp 被设计为是线程安全的,并且在创建文件或目录时会检查潜在的竞赛条件。如果 mktemp 命令发现指定的文件或目录已经存在,它将尝试创建一个新的,直到找到一个不存在的名称。

示例:在脚本中使用 mktemp

#!/bin/bash

# 创建一个临时文件
tempfile=$(mktemp)
echo "临时文件被创建在:$tempfile"

# 使用临时文件执行一些操作...

# 完成后,可以删除临时文件
# rm "$tempfile"

安装复制文件install

install 是一个常用的Unix命令行工具,用于复制文件并根据需要设置它们的权限。

基本语法:

install [OPTION]... SOURCE... DESTINATION
  • SOURCE:一个或多个源文件或目录的路径。
  • DESTINATION:目标文件或目录的路径。

常用选项:

  • -c:复制文件,这是默认行为。
  • -d:创建目标为目录。
  • -m:设置目标文件的权限模式(类似于 chmod)。
  • -o:设置目标文件的所有者。
  • -g:设置目标文件的组。
  • -p:保留源文件的修改时间、访问时间和权限模式。
  • -s:复制文件并设置目标为可执行文件。
  • -v:详细模式,显示被安装的文件的详细信息。

示例:

  1. 复制文件并设置权限
install -m 755 source_file destination_file

这将复制 source_filedestination_file 并设置目标文件的权限为 755

  1. 复制目录
install -d -m 755 source_directory destination_directory

这将复制 source_directorydestination_directory 并设置目标目录的权限为 755

  1. 保留文件属性
install -p source_file destination_file

这将复制 source_file 并保留其修改时间、访问时间和权限。

  1. 设置所有者和组
install -o user -g group source_file destination_file

这将设置目标文件的所有者为 user,组为 group

  1. 复制并使文件可执行
install -s source_file destination_file

这将复制 source_file 并使目标文件 destination_file 变为可执行。

  1. 详细模式
install -v source_file destination_file

这将显示复制过程中的详细信息。

提示:

  • 使用 install 命令时,需要确保你有足够的权限来写入目标位置。
  • 默认情况下,如果目标文件已存在,install 会覆盖它,除非使用了 -d 选项,此时如果目标目录不存在,install 会尝试创建它。
  • install 命令在构建系统和软件包管理器中非常常见,用于自动化安装过程。

交互式转化批处理工具expect

expect 是一个用于自动化交互式应用程序的工具,特别是那些需要用户输入的程序。它由Don Libes和Greg Fedorczyk在1990年代初期开发,最初是作为Tcl(Tool Command Language)的一个扩展。expect 能够自动发送预定义的输入(如密码、命令选项等)给那些需要交互的程序,从而实现自动化脚本。

主要特点:

  1. 自动化交互expect 可以自动与需要用户输入的程序进行交互。
  2. 模式匹配:它使用模式匹配来识别程序的输出,并根据这些输出决定下一步的行动。
  3. 非阻塞expect 能够等待特定的字符串或模式出现,而不是盲目地发送输入,这使得它可以处理复杂的交互场景。

基本用法:

#!/usr/bin/expect

# 设置超时时间
set timeout 20

# 启动程序
spawn ssh user@remote_host

# 等待 "password:" 字符串出现
expect "password:"

# 发送密码并按回车
send "mypassword\r"

# 等待一个特定的模式,比如 "$ " 或 "# "
expect "$ "

核心概念:

  • spawn:启动一个交互式程序。
  • expect:等待特定的字符串或模式出现。
  • send:向交互式程序发送输入。
  • wait:等待特定的条件成立。

高级用法:

  • 非阻塞等待expect 可以等待多个条件中的任何一个发生,并在第一个条件满足时继续执行。
  • 复杂的交互:可以编写复杂的脚本来处理多步骤的交互过程。
  • 错误处理:可以捕获和处理交互过程中的错误。

示例:

#!/usr/bin/expect

# 尝试登录到远程服务器
set timeout 20
set password "mypassword"

spawn ssh user@remote_host

# 等待 "password:" 或 "Permission denied" 并处理
expect {
    "password:" { send "$password\r" }
    "Permission denied" { exit 1 }
    timeout { exit 2 }
}

# 等待命令行提示符
expect "$ "

# 发送命令
send "ls -l\r"

# 等待命令执行完成
expect "$ "

# 退出SSH会话
send "exit\r"
expect eof

这个脚本尝试使用SSH登录到远程服务器,自动输入密码,发送 ls -l 命令,然后退出。

提示:

  • expect 是Tcl脚本语言的一部分,因此需要Tcl环境来运行。
  • 它通常用于自动化需要密码或其他交互的脚本,特别是在网络设备、服务器管理等场景中。
  • expect 脚本可以非常强大和复杂,能够处理各种交互式应用程序。

数组

在Shell脚本中,数组是一种非常有用的数据结构,用于存储多个值。Bash(Bourne Again SHell)支持一维数组和关联数组(从Bash 4开始)。

一维数组

一维数组是最基本的数组类型,可以存储一系列的值。

定义数组

# 定义数组并初始化
array=(value1 value2 value3)

# 单独初始化数组元素
array[0]=value1
array[1]=value2
array[2]=value3

访问数组元素

# 访问第一个元素
echo ${array[0]}

# 访问第二个元素
echo ${array[1]}

获取数组长度

# 获取数组元素个数
echo ${#array[@]}

# 获取数组某个维度的长度(仅一维数组时,返回值总是1)
echo ${#array[0]}

遍历数组

# 遍历数组
for item in "${array[@]}"; do
    echo $item
done

关联数组

关联数组允许使用字符串作为索引,这在需要通过名称访问元素时非常有用。

定义关联数组

# 定义关联数组并初始化
declare -A assoc_array
assoc_array=([key1]=value1 [key2]=value2 [key3]=value3)

# 单独初始化关联数组元素
assoc_array[key1]=value1
assoc_array[key2]=value2

访问关联数组元素

# 访问关联数组元素
echo ${assoc_array[key1]}

获取关联数组长度

# 获取关联数组的键的数量
echo ${#assoc_array[@]}

# 获取关联数组的值的数量
echo ${#assoc_array[*]}

遍历关联数组

# 遍历关联数组的键和值
for key in "${!assoc_array[@]}"; do
    echo "$key: ${assoc_array[$key]}"
done

高阶使用

  1. 数组排序: 使用 sort 命令对数组进行排序。
array=("banana" "apple" "cherry")
printf "%s\n" "${array[@]}" | sort
  1. 数组的数组: 使用数组的数组来模拟多维数组。
matrix=([0]="apple" [1]="banana" [2]="cherry")
echo ${matrix[1]}
  1. 数组操作: 使用 += 操作符向数组添加元素。
array+=("orange")
  1. 数组的默认值: 使用 ${array[@]:+value} 来为数组元素设置默认值。
echo "${array[0]:-default_value}"
  1. 数组的间接引用: 使用 ${!array_name[@]} 获取数组的所有索引。
echo ${!array[@]}

数组应用案例

  1. 读取文件行到数组: 读取文件的每一行到数组中。
lines=()
while IFS= read -r line; do
    lines+=("$line")
done < "file.txt"

echo "${lines[@]}"
  1. 处理命令行参数: 将脚本的所有命令行参数存储到数组中。
arguments=("$@")
echo "Arguments: ${arguments[@]}"
  1. 使用数组作为队列: 使用数组实现队列操作。
queue=()
push() { queue+=("$1") }
pop() { [ ${#queue[@]} -gt 0 ] && queue=${queue[@]:1} }

push "apple"
push "banana"
pop
echo "${queue[@]}"
  1. 生成10个随机数并保存在数组中,找出它们的最大值和最小值。

脚本解释:

  1. 初始化数组:使用 random_numbers=() 初始化一个空数组。
  2. 生成随机数:使用 for 循环和 $RANDOM 变量生成10个随机数,并将它们添加到数组中。$RANDOM 是一个特殊变量,每次读取时都会生成一个0到32767之间的随机整数。
  3. 设置初始最大值和最小值:将数组的第一个元素分别赋值给 maxmin 变量。
  4. 找出最大值和最小值:使用另一个 for 循环遍历数组中的每个元素,并与当前的 maxmin 进行比较,更新它们的值。
  5. 输出结果:使用 echo 命令输出生成的随机数数组、最大值和最小值。
#!/bin/bash

# 初始化数组
random_numbers=()

# 生成10个随机数并保存到数组中
for i in {1..10}; do
    random_numbers+=($RANDOM)
done

# 设置初始最大值和最小值
max=${random_numbers[0]}
min=${random_numbers[0]}

# 遍历数组,找出最大值和最小值
for number in "${random_numbers[@]}"; do
    if [ $number -gt $max ]; then
        max=$number
    elif [ $number -lt $min ]; then
        min=$number
    fi
done

# 输出结果
echo "生成的随机数:${random_numbers[@]}"
echo "最大值:$max"
echo "最小值:$min"

字符串处理

Shell脚本中处理字符串包括字符串的截取、替换、拼接、查找和比较等操作。

1. 字符串截取

在Shell中,可以使用花括号扩展(Brace Expansion)和参数扩展来截取字符串。

  • 使用 ${string:position:length}
  • position 是开始截取的位置(0 表示字符串的开始)。
  • length 是要截取的字符长度。
str="Hello, World!"
echo ${str:0:5}  # 输出 "Hello"
echo ${str:7:5}  # 输出 "World"

2. 字符串替换

  • 使用 // 进行替换
  • 第一个字符串是被替换的模式。
  • 第二个字符串是替换后的字符串。
str="Hello, World!"
echo ${str//,/ and}  # 输出 "Hello and World!"

3. 字符串拼接

  • 使用双引号或加号
  • 拼接字符串时,可以使用双引号将它们包含在一起,或者使用加号。
str1="Hello"
str2="World"
echo "${str1} ${str2}"  # 输出 "Hello World"
echo $str1$str2  # 输出 "HelloWorld"

4. 字符串查找

  • 使用 =~ 进行正则表达式匹配
  • 可以在 [[ ]] 中使用 =~ 来检查字符串是否匹配正则表达式。
str="Hello, World!"
if [[ $str =~ [Hh]ello ]]; then
    echo "String starts with 'Hello' or 'hello'"
fi

5. 字符串比较

  • 使用 -z-n
  • -z 检查字符串是否为空。
  • -n 检查字符串是否非空。
str=""
if [ -z "$str" ]; then
    echo "String is empty"
else
    echo "String is not empty"
fi

str="Hello"
if [ -n "$str" ]; then
    echo "String is not empty"
fi
  • 使用 =!=
  • 比较两个字符串是否相等或不等。
str1="Hello"
str2="World"

if [ "$str1" = "$str2" ]; then
    echo "Strings are equal"
else
    echo "Strings are not equal"
fi

6. 字符串长度

  • 使用 ${#string}
  • 获取字符串的长度。
str="Hello, World!"
echo ${#str}  # 输出 "13"

7. 删除字符串中的字符

  • 使用 %#
  • % 删除最短匹配的前缀。
  • # 删除最长匹配的前缀。
str="Hello, World!"
echo ${str%,*}  # 输出 "Hello"
echo ${str#*,}   # 输出 " World!"

8. 字符串的默认值

  • 使用 ${var:-default}
  • 如果 var 未设置或为空,则使用 default
str=""
echo ${str:-"default value"}  # 输出 "default value"

9. 字符串的算术运算

  • 使用 ((...))
  • 进行算术运算。
str="123"
echo $((${str} * 2))  # 输出 "246"

10. 字符串的模式匹配

  • 使用 [[ ]]
  • 检查字符串是否符合特定的模式。
str="Hello"
if [[ $str == H* ]]; then
    echo "String starts with 'H'"
fi

11. 字符串处理案例

场景:批量重命名文件。

假设有一个包含多个文件的目录,文件名如下:

file1.txt
file2.txt
file3.txt
...

将这些文件重命名为以下格式:

data_file1.txt
data_file2.txt
data_file3.txt
...

使用循环和字符串替换:

#!/bin/bash

# 定义文件名的前缀
prefix="data_"

# 进入包含文件的目录
cd /path/to/directory

# 遍历当前目录下的所有 .txt 文件
for file in *.txt; do
    # 使用 mv 命令和字符串替换来重命名文件
    mv "$file" "${file/.txt/${prefix}$file.txt}"
done

高级变量

在Shell脚本中,特别是Bash,虽然不像一些高级编程语言那样有显式的“类型”声明,但可以通过一些约定和技巧来实现类似“类型”变量的处理。

以下是一些高级变量赋值和用法的示例。

1. 整数变量

在Bash中,可以通过 declaretypeset 命令将变量声明为整数类型,确保变量只包含整数值。

# 声明一个整数变量
declare -i int_var

# 赋值
int_var=42

# 使用
((int_var++))
echo $int_var  # 输出: 43

2. 浮点数变量

Bash本身不支持浮点数,但可以通过外部工具如 bc 来处理浮点数。

# 使用 bc 处理浮点数
float_var="10.5"
echo "Scale=2; $float_var / 3" | bc  # 输出: 3.5

3. 字符串变量

字符串变量是Shell中最常用的变量类型,可以通过双引号或单引号来定义。

# 定义字符串变量
str_var="Hello, World!"

# 访问字符串变量
echo $str_var

4. 布尔变量

Bash中的布尔变量实际上是通过整数来模拟的,0表示假,非0表示真。

# 定义布尔变量
bool_var=true

# 检查布尔变量
if [ "$bool_var" = true ]; then
    echo "Boolean is true"
fi

5. 只读变量

使用 readonly 命令可以将变量设置为只读,防止在脚本中被修改。

# 定义只读变量
readonly read_only_var="I cannot be changed"

# 尝试修改只读变量将导致错误
read_only_var="New value"  # 这将不改变 read_only_var 的值

6. 数组变量

Bash支持一维和关联数组,数组变量可以存储多个值。

# 定义一维数组
array_var=("apple" "banana" "cherry")

# 定义关联数组
declare -A assoc_array
assoc_array=([key1]="value1" [key2]="value2")

7. 动态变量名

可以使用变量的变量(indirect expansion)来动态地引用变量名。

# 定义变量的变量
var_name="str_var"
var_value="Hello, World!"
$var_name="$var_value"

# 访问变量的变量
echo ${!var_name}  # 输出: str_var
echo ${str_var}    # 输出: Hello, World!

8. 命名引用

Bash 4.3及更高版本支持命名引用(nameref),允许变量名存储在另一个变量中。

# 定义命名引用
nameref_var=str_var

# 通过命名引用访问变量
$nameref_var="New value"
echo ${!nameref_var}  # 输出: New value

9. 变量的默认值

可以使用 ${variable:-default} 语法为变量提供默认值。

# 定义带默认值的变量
var_with_default="${undefined_var:-default_value}"

echo $var_with_default  # 输出: default_value

10. 变量的算术运算

可以使用 ((...)) 来进行算术运算。

# 定义整数变量并进行算术运算
int_var=10
((int_var *= 2))
echo $int_var  # 输出: 20

11. 间接变量引用

在Shell脚本中,间接变量引用(Indirect Variable Reference)允许通过一个变量的值来引用另一个变量。 这在处理动态变量名或需要根据某些条件来决定变量名时非常有用。

间接变量引用的基本语法如下:

${variable_name}

这里的 variable_name 是存储另一个变量名的变量。

假设有两个变量名存储在变量 var_name 中,我们可以使用间接变量引用来访问这些变量的值:

# 定义变量
var1="value1"
var2="value2"

# 存储变量名的变量
var_name="var1"

# 使用间接变量引用访问 var1 的值
echo ${!var_name}  # 输出: value1

# 改变 var_name 的值,访问 var2 的值
var_name="var2"
echo ${!var_name}  # 输出: value2

高级用法:

  1. 动态变量名: 使用间接变量引用来处理动态生成的变量名。
for i in {1..3}; do
    var$i="value$i"
done

echo ${!var1}  # 输出: value1
echo ${!var2}  # 输出: value2
echo ${!var3}  # 输出: value3
  1. 数组索引的变量: 使用间接变量引用来处理数组索引的变量。
array=("apple" "banana" "cherry")

# 存储索引的变量
index="1"

# 使用间接变量引用访问数组元素
echo ${array[$index]}  # 输出: banana
  1. 关联数组的键: 使用间接变量引用来处理关联数组的键。
declare -A assoc_array
assoc_array["key1"]="value1"
assoc_array["key2"]="value2"

# 存储键的变量
key="key1"

# 使用间接变量引用访问关联数组元素
echo ${assoc_array[$key]}  # 输出: value1
  1. 函数返回的变量名: 使用间接变量引用来处理函数返回的变量名。
function_name() {
    echo "var1"
}

var_name=$(function_name)
echo ${!var_name}  # 输出: value1
  1. 读取命令的输出: 使用间接变量引用来处理读取命令的输出。
echo "var1" > var_file

source var_file
echo ${!var_name}  # 输出: value1

提示:

  • 确保存储变量名的变量(如 var_name)已经定义,并且其值是有效的变量名。
  • 间接变量引用在处理复杂的数据结构和动态变量名时非常有用,但也可能增加脚本的复杂性,因此需要谨慎使用。

11. eval命令

eval 命令会将字符串作为命令行参数进行解析,并执行相应的命令。

基本用法:

eval [options] [arguments]
  • options:可选的命令行选项。
  • arguments:要执行的命令字符串。

示例:

  1. 执行简单的命令
cmd="echo Hello, World!"
eval $cmd
  1. 执行多个命令
cmds="echo 'First command'; echo 'Second command'"
eval "$cmds"
  1. 使用变量
var="Hello, World!"
eval "echo $var"

高级用法:

  1. 处理复杂的命令

eval 可以执行包含引号、空格和特殊字符的复杂命令。

cmd='echo "Hello, \"World!\""'
eval "$cmd"
  1. 从文件中读取命令
# 假设 file.txt 包含以下内容:
# echo "Command from file"

eval $(cat file.txt)
  1. 处理用户输入
read -p "Enter a command: " user_cmd
eval $user_cmd
  1. 使用 eval 进行字符串替换
str="Hello World"
eval "str='${str// /_}'"
echo $str  # 输出: Hello_World
  1. 在循环中使用 eval
for cmd in "echo 'First'" "echo 'Second'"; do
    eval "$cmd"
done

提示:

  • 安全性eval 会执行传递给它的任何命令,这可能包括恶意代码。因此,使用 eval 时需要非常小心,避免执行不可信的输入。
  • 引号:在使用 eval 时,通常需要将命令字符串用引号包围,以确保字符串中的空格和特殊字符被正确处理。
  • 变量替换eval 可以处理命令字符串中的变量替换,但需要注意变量的引用方式。

下面是一些安全使用 eval 的建议:

  1. 避免执行不可信的输入:不要对用户输入或其他不可信的源使用 eval
  2. 限制命令的执行:如果可能,使用更安全的方式,如 $(...) 命令替换或 read 命令。
  3. 使用 set 命令:在执行 eval 之前,使用 set 命令限制可执行的命令类型。
set -f  # 禁止函数和内置命令的执行
eval "$cmd"
set +f