Shell脚本编程


文档摘要

Shell脚本编程 脚本的需求 Hello脚本 加载脚本 命令行参数 变量和比较 交互式接受用户输入 if then else for循环 while循环 读取文件 调试 实际应用场景 资源列表 脚本的需求 自动化重复的手动任务 创建专门的自定义命令 脚本语言与编程语言的区别 注意: 通常用作壳脚本的扩展名 此处提供的材料是针对 的 Hello脚本 第一行有两个部分 is path of to get path called as shebang), directs the program loader to use the interpreter path provided Comments Comments start with Comments can be placed at

Shell脚本编程


脚本的需求

注意:

  • .sh 通常用作壳脚本的扩展名
  • 此处提供的材料是针对 GNU bash, version 4.3.11(1)-release

Hello脚本

#!/bin/bash # Print greeting message echo "Hello $USER" # Print day of week echo "Today is $(date -u +%A)" # use single quotes for literal strings echo 'Have a nice day'

第一行有两个部分

  • /bin/bash is path of bash
    • 类型 bash to get path
  • #! called as shebang, directs the program loader to use the interpreter path provided

Comments

  • Comments start with #
  • Comments can be placed at end of line of code as well
    • echo 'Hello' # 代码注释结尾
  • Multiline comments

Single quotes vs Double quotes

  • Single quotes preserves the literal value of each character within the quotes
  • Double quotes preserves the literal value of all characters within the quotes, with the exception of '$', '``, '', 当启用历史扩展时,还有 '!'
  • 单引号和双引号的区别

echo 内置命令

  • help -d echo 将参数写入标准输出
  • 默认情况下,echo 会添加一个换行符且不解释反斜杠
  • -n 不添加换行符
  • -e 启用对后续反斜杠转义字符的解释
  • -E 显式抑制对反斜杠转义字符的解释
  • 关于echo的问答
$ chmod +x hello_script.sh $ ./hello_world.sh Hello learnbyexample Today is Wednesday Have a nice day

加载脚本

$ help -d source source - Execute commands from a file in the current shell.
  • 如果脚本应在当前shell环境中而不是子shell中执行,请使用. or source command
    • For example, after editing ~/.bashrc one can use source ~/.bashrc使更改立即生效
$ # contents of prev_cmd.sh prev=$(fc -ln -2 | sed 's/^[ \t]*//;q') echo "$prev"
  • 例如,从脚本内部访问当前交互式shell的历史记录
$ printf 'hi there\n' hi there $ bash prev_cmd.sh $ printf 'hi there\n' hi there $ source prev_cmd.sh printf 'hi there\n'

命令行参数

#!/bin/bash # Print line count of files given as command line argument echo "No of lines in '$1' is $(wc -l < "$1")" echo "No of lines in '$2' is $(wc -l < "$2")"
  • 命令行参数保存在以 $1 $2 $3 etc
  • If a particular argument requires multiple word string, enclose them in quotes or use appropriate escape sequences
  • $0 contains the name of the script itself - useful to code different behavior based on name of script used
  • $@ array of all the command line arguments passed to script
  • $# 开头的位置变量中。$# 表示传递给脚本的命令行参数的数量。
  • 在将变量值传递给另一个命令时,使用双引号包围变量。
  • bash特殊参数参考
$ ./command_line_arguments.sh hello_script.sh test\ file.txt No of lines in 'hello_script.sh' is 9 No of lines in 'test file.txt' is 5

变量和比较

  • dir_path=/home/guest space has special meaning in bash, cannot be used around = in variables
  • greeting='hello world' use single quotes for literal strings
  • user_greeting="hello $USER" use double quotes for substitutions
  • echo $user_greeting use $ when variable's value is needed
  • no_of_lines=$(wc -l < "$filename") use double quotes around variables when passing its value to another command
  • num=534 numbers can also be declared
  • (( num = 534 )) but using (( )) for numbers makes life much easier
  • (( num1 > num2 )) number comparisons are also more readable within (( ))
  • [[ -e story.txt ]] test if the file/directory exists
  • [[ $str1 == $str2 ]] 用于字符串比较

进一步阅读


交互式接受用户输入

#!/bin/bash # Get user input echo 'Hi there! This script returns the sum of two numbers' read -p 'Enter two numbers separated by spaces: ' number1 number2 echo -e "\n$number1 + $number2 = $((number1 + number2))" echo 'Thank you for using the script, Have a nice day :)'
  • help -d read Read a line from the standard input and split it into fields
  • -a array assign the words read to sequential indices of the array variable ARRAY, starting at zero
  • -p prompt output the string PROMPT without a trailing newline before attempting to read
  • -s 不回显来自终端的输入
$ ./user_input.sh Hi there! This script returns the sum of two numbers Enter two numbers separated by spaces: 7 42 7 + 42 = 49 Thank you for using the script, Have a nice day :)

if then else

#!/bin/bash if (( $# != 2 )) then echo "Error!! Please provide two file names" # simple convention for exit values is '0' for success and '1' for error exit 1 else # Use ; to combine multiple commands in same line # -f option checks if file exists, ! negates the value # white-space around [[ and ]] is necessary if [[ ! -f $1 ]] ; then echo "Error!! '$1' is not a valid filename" ; exit 1 else echo "No of lines in '$1' is $(wc -l < "$1")" fi # Conditional Execution [[ ! -f $2 ]] && echo "Error!! '$2' is not a valid filename" && exit 1 echo "No of lines in '$2' is $(wc -l < "$2")" fi
  • 在处理用户提供的参数时,始终建议检查参数的有效性。简单的检查可以在出问题时减少数小时令人沮丧的调试时间。
  • 代码中的 if [[ ! -f $1 ]] ; then block is only intended for demonstration, we could as well have used error handling of wc command if file doesn't exist
  • Default exit value is 0 , so need not be explicitly written for successful script completion
  • Use elif if you need to test more conditions after if
  • The operator && is used to execute a command only when the preceding one successfully finishes
  • To redirect error message to stderr, use echo "错误!! 请提供两个文件名" 1>&2 等等
  • 控制运算符 && 和 ||
  • 更多if条件块的例子
$ ./if_then_else.sh Error!! Please provide two file names $ echo $? 1 $ ./if_then_else.sh hello_script.sh Error!! Please provide two file names $ echo $? 1 $ ./if_then_else.sh hello_script.sh xyz.tzt No of lines in 'hello_script.sh' is 9 Error!! 'xyz.tzt' is not a valid filename $ echo $? 1 $ ./if_then_else.sh hello_script.sh 'test file.txt' No of lines in 'hello_script.sh' is 9 No of lines in 'test file.txt' is 5 $ echo $? 0

结合if和命令执行状态

有时需要知道预期命令操作是否成功,并根据结果采取行动。退出状态为 0 is considered as successful condition when used with if 语句。如果可用,使用适当的选项来抑制命令的stdout/stderr,否则可能需要重定向以避免在终端上造成混乱。

$ grep 'echo' hello_script.sh echo "Hello $USER" echo "Today is $(date -u +%A)" echo 'Have a nice day' $ # do not write anything to standard output $ grep -q 'echo' hello_script.sh $ echo $? 0 $ grep -q 'echo' xyz.txt grep: xyz.txt: No such file or directory $ echo $? 2 $ # Suppress error messages about nonexistent or unreadable files $ grep -qs 'echo' xyz.txt $ echo $? 2

例子

#!/bin/bash if grep -q 'echo' hello_script.sh ; then # do something echo "string found" else # do something else echo "string not found" fi

for循环

#!/bin/bash # Ensure atleast one argument is provided (( $# == 0 )) && echo "Error!! Please provide atleast one file name" && exit 1 file_count=0 total_lines=0 # every iteration, variable file gets next positional argument for file in "$@" do # Let wc show its error message if file doesn't exist # terminate the script if wc command exit status is not 0 no_of_lines=$(wc -l < "$file") || exit 1 echo "No of lines in '$file' is $no_of_lines" ((file_count++)) ((total_lines = total_lines + no_of_lines)) done echo -e "\nTotal Number of files = $file_count" echo "Total Number of lines = $total_lines"
  • 这种形式的 for loop is useful if we need only element of an array, without having to iterate over length of an array and using an index for each iteration to get array elements
  • In this example we use the control operator || to stop the script if wc fails i.e 'exit status' other than 0
$ ./for_loop.sh Error!! Please provide atleast one file name $ echo $? 1 $ ./for_loop.sh hello_script.sh if_then_else.sh command_line_arguments.sh No of lines in 'hello_script.sh' is 9 No of lines in 'if_then_else.sh' is 21 No of lines in 'command_line_arguments.sh' is 5 Total Number of files = 3 Total Number of lines = 35 $ echo $? 0 $ ./for_loop.sh hello_script.sh xyz.tzt No of lines in 'hello_script.sh' is 9 ./for_loop.sh: line 14: xyz.tzt: No such file or directory $ echo $? 1

基于索引的for循环

#!/bin/bash # Print 0 to 4 for ((i = 0; i < 5; i++)) do echo $i done

迭代用户定义的数组

$ files=('report.log' 'pass_list.txt') $ for f in "${files[@]}"; do echo "$f"; done report.log pass_list.txt

由通配符模式指定的文件

常见的错误是使用 ls 命令的输出,这容易出错且没有必要。相反,可以直接使用这些参数。

$ ls pass_list.txt power.log report.txt $ for f in power.log *.txt; do echo "$f"; done power.log pass_list.txt report.txt

while循环

#!/bin/bash # Print 5 to 1 (( i = 5 )) while (( i != 0 )) do echo $i ((i--)) done
  • 当需要根据指定条件执行命令时,请使用 while 循环
$ ./while_loop.sh 5 4 3 2 1

读取文件

逐行读取

#!/bin/bash while IFS= read -r line; do # do something with each line echo "$line" done < 'files.txt'
  • IFS is used to specify field separator which is by default whitespace. IFS= will clear the default value and prevent stripping of leading and trailing whitespace of lines
  • The -r option for read will prevent interpreting \ 转义
  • 如果输入的最后一行没有被换行符正确终止,则不会读取最后一行
$ cat files.txt hello_script.sh if_then_else.sh $ ./while_read_file.sh hello_script.sh if_then_else.sh

按不同字段读取行

  • 默认情况下,空白字符作为分隔符
  • 通过设置 IFS 指定不同的分隔符
$ cat read_file_field.sh #!/bin/bash while IFS=: read -r genre name; do echo -e "$genre\t:: $name" done < 'books.txt' $ cat books.txt fantasy:Harry Potter sci-fi:The Martian mystery:Sherlock Holmes $ ./read_file_field.sh fantasy :: Harry Potter sci-fi :: The Martian mystery :: Sherlock Holmes

一次读取'n'个字符

$ while read -n1 char; do echo "Character read is: $char"; done <<< "\word" Character read is: w Character read is: o Character read is: r Character read is: d Character read is: $ # if ending newline character is not desirable $ while read -n1 char; do echo "Character read is: $char"; done < <(echo -n "hi") Character read is: h Character read is: i $ while read -r -n2 chars; do echo "Characters read: $chars"; done <<< "\word" Characters read: \w Characters read: or Characters read: d

调试

  • -x Print commands and their arguments as they are executed
  • -v verbose option, print shell input lines as they are read
  • set -xv 使用此命令从脚本内部启用调试
$ bash -x hello_script.sh + echo 'Hello learnbyexample' Hello learnbyexample ++ date -u +%A + echo 'Today is Friday' Today is Friday + echo 'Have a nice day' Have a nice day
$ bash -xv hello_script.sh #!/bin/bash # Print greeting message echo "Hello $USER" + echo 'Hello learnbyexample' Hello learnbyexample # Print day of week echo "Today is $(date -u +%A)" date -u +%A ++ date -u +%A + echo 'Today is Friday' Today is Friday # use single quotes for literal strings echo 'Have a nice day' + echo 'Have a nice day' Have a nice day

实际应用场景

创建这些章节时,涉及大量复制粘贴命令及其输出,因此难免会出现错误。因此,编写一个脚本来检查正确性非常有用。考虑以下markdown文件

## <a name="some-heading"></a>Some heading 一些解释 ```bash $ seq 3 1 2 3 $ printf 'hi there!\n' hi there! ``` ## <a name="another-heading"></a>Another heading 更多解释 ```bash $ help -d readarray readarray - Read lines from a file into an array variable. $ a=5 $ printf "$a\n" 5 ```
  • 整个文件被读入一个数组,以便可以动态地控制要读取的下一行的索引。
  • 一旦识别出要测试的命令
    • 将预期输出收集到一个变量中。多行输出会被连接在一起。有些命令没有标准输出供比较。
    • 相应地调整下一个迭代的索引。
  • 请注意,这是一个演示shell脚本用途的示例脚本。它并不是万无一失的,没有主动检查可能的错误等等。
  • 确保eval用于已知命令,就像这里的情况一样。
  • 参见 参数扩展 以了解字符串处理结构的示例和解释。
#!/bin/bash cb_start=0 readarray -t lines < 'sample.md' for ((i = 0; i < ${#lines[@]}; i++)); do # mark start/end of command block # Line starting with $ to be verified only between ```bash 和 ``` block end [[ ${lines[$i]:0:7} == '```bash' ]] && ((cb_start=1)) && continue [[ ${lines[$i]:0:3} == '```' ]] && ((cb_start=0)) && continue if [[ $cb_start == 1 && ${lines[$i]:0:2} == '$ ' ]]; then cmd="${lines[$i]:2}" # collect command output lines until line starting with $ or ``` block end cmp_str='' j=1 while [[ ${lines[$i+$j]:0:2} != '$ ' && ${lines[$i+$j]:0:3} != '```' ]]; do cmp_str+="${lines[$i+$j]}" ((j++)) done ((i+=j-1)) cmd_op=$(eval "$cmd") if [[ "${cmd_op//$'\n'}" == "${cmp_str//$'\n'}" ]]; then echo "Pass: $cmd" else echo "Fail: $cmd" fi fi done
  • 注意到通过源码执行脚本有助于考虑到依赖于前一条命令的命令。
$ ./verify_cmds.sh Pass: seq 3 Pass: printf 'hi there!\n' Pass: help -d readarray Pass: a=5 Fail: printf "$a\n" $ source verify_cmds.sh Pass: seq 3 Pass: printf 'hi there!\n' Pass: help -d readarray Pass: a=5 Pass: printf "$a\n"

资源列表

本章的内容只是一个简单的介绍。

Shell 脚本

特定主题

实用工具、技巧和参考

致谢:github.com/learnbyexample/Linux_command_line

声明:
本文件灏天文库团队进行了翻译。尽管我们力求准确,但请注意,翻译可能包含错误或不准确之处。原文档以其原始语言为准。我们不对因使用此翻译而产生的任何误解或误译负责。


作者与出处
原作者: Tikam02
来源:Tikam02
许可证:MIT
整理: 灏天文库整理
由灏天文库结构化整理,提供目录导航、全文检索与在线阅读,便于系统化学习
发布者: 作者: Tikam02 转发
评论区 (0)
U