65.9K
CodeProject 正在变化。 阅读更多。
Home

无人谈论的Shell编程秘诀(第二部分)

starIconstarIconstarIconstarIconstarIcon

5.00/5 (3投票s)

2023 年 3 月 7 日

CPOL

9分钟阅读

viewsIcon

9258

本系列文章的最后一部分涵盖了 bash 的其他一些重要行为,特别是与文本展开和替换相关的行为。

引言

本系列文章的最后一部分涵盖了 bash 的其他一些重要行为,特别是与文本展开和替换相关的行为。

Bourne shell (sh) 著名的文本处理能力在 Bourne-Again shell (bash) 中得到了极大的扩展。这些能力非常高效,代码甚至可能更加晦涩。这使得 bash 命令解释器和编程语言非常强大,但也可能是一个错误的雷区。

在继续之前,让我们回顾一下我们在文章第一部分中学到的内容

  • shbash 不同
  • if 语句检查退出码(0 表示 true,非零表示 false),而不是布尔值
  • [truefalse 是程序,不是关键字
  • 变量比较和赋值中空格的存在/缺失会产生很大影响
  • [[ 不是 [ 的安全备用版本
  • 算术运算不像其他语言那样直接
  • 数组操作使用晦涩的运算符
  • 默认情况下,bash 不会因错误而停止

在这一部分,我们将重点介绍 bash 如何执行文本展开和替换。我将只介绍我认为最重要的文本处理功能。如需全面了解,您需要查阅Bash 参考手册。Bash 和 sedgrep 等多个命令也使用正则表达式进行文本处理。正则表达式本身是一个独立的主题,我在这里也不会涵盖它们。

历史扩展字符(!)

此功能在从 shell 提示符输入命令时可用。它用于访问存储在 bash 历史文件中的命令。

!n 执行 bash 历史记录中的第 n 条命令
!! 执行最后一条命令(相当于 !-1
!leword 执行最后一条以“leword”开头的命令
!?leword? 执行最后一条包含“leword”的命令
^search^replace 在替换“search”的第一个出现项为“replace”后执行最后一条命令

您可以使用某些单词指示符修改历史搜索,这些指示符前面加上冒号(:)。

!?leword?:0 使用包含“leword”的最后一条命令中的第 0 个单词(通常是命令可执行文件)执行。
!?leword?:2 使用包含“leword”的最后一条命令中的第二个单词执行。
!?leword?:$ 使用包含“leword”的最后一条命令中的最后一个单词执行。
!?leword?:2-6 使用包含“leword”的最后一条命令中的第二个到第六个单词执行。
!?leword?:-6 使用包含“leword”的最后一条命令中直到第 6 个单词的所有单词执行(相当于 !?leword?:0-6)
!?leword?:* 使用包含“leword”的最后一条命令中的所有单词执行(但不包括第 0 个单词)(相当于 !?leword?:1-$
!?leword?:2* 使用包含“leword”的最后一条命令中的第二个单词到最后一个单词执行(但不包括第 0 个单词)(相当于 !?leword?:2-$)
!?leword?:2- 使用包含“leword”的命令中从第 2 个位置到倒数第二个单词的所有单词执行,但不包括第 0 个单词。

请记住,bash 会执行您从历史记录中检索到的内容以及您在提示符中已经键入的内容。您还可以使用任何数量的修改器,每个修改器前面都带有一个冒号(:)。

!?leword?:p 显示(但不执行)包含“leword”的最后一条命令
!?leword?:t 在移除最后一个参数的所有路径名后(即仅保留包含文件名的一部分)执行包含“leword”的最后一条命令
!?leword?:r 在移除最后一个参数的文件扩展名后执行包含“leword”的最后一条命令
!?leword?:e 在移除最后一个参数的路径名和文件名后(仅留下扩展名)执行包含“leword”的最后一条命令
!?leword?:s/search/replace 在替换“search”的第一个实例为“replace”后执行包含“leword”的最后一条命令
!?leword?:as/search/replace 在替换“search”的所有实例为“replace”后执行包含“leword”的最后一条命令

如果您省略搜索文本('leword')并使用历史扩展字符以及单词指示符和修改器,bash 将搜索最后一条命令。在您熟练使用历史扩展字符之前,请使用修改器 :p 在实际执行命令之前显示该命令。

文本展开和替换

这些功能在 shell 提示符和 shell 脚本中都可用。

  • 波浪号 (~):在您的命令中,bash 会将 ~ 的实例展开为环境变量 $HOME 的值,即您的主目录。
  • ?*:这些是元字符。在文件描述符中,? 匹配任何单个字符,而 * 匹配任意数量的任意字符。如果它们不匹配任何文件名,bash 将使用它们的字面值。
  • 花括号扩展:您可以使用花括号内逗号分隔的文本字符串来生成具有后缀和/或前缀的字符串组合。当我开始写新书时,我会这样创建文件夹。
    mkdir -p NewBook/{ebook/images,html/images,image-sources,isbn,pub,ref}
    此命令会创建类似这样的文件夹
    NewBook
    NewBook/ref
    NewBook/pub
    NewBook/isbn
    NewBook/image-sources
    NewBook/html
    NewBook/html/images
    NewBook/ebook
    NewBook/ebook/images 
  • 参数扩展:当 bash 执行脚本时,它会为脚本创建这些特殊变量。
    Shell 变量 用途
    $0 Shell 脚本的名称
    $1, $2,… 传递给脚本的位置参数或参数
    $# 传递给脚本的参数总数
    $? 最后一条命令的退出状态
    $* 所有参数(双引号括起来)
    $@ 所有参数(单独双引号括起来)
    $$ 当前 shell/脚本的进程 ID

    在终端上,$0 通常会展开为 shell 程序(/bin/bash)。在终端上,您可以使用 set 命令事实上为当前 shell 指定参数。

    # Displays 0
    echo $#
    
    # Displays an empty string and causes a new line
    echo $*
    
    # Sets hello and world as parameters to current shell
    set -- hello world
    
    # Displays 2 (the number of parameters)
    echo $#
    
    # Displays hello world
    echo $*
    
    # Remove parameters to current shell
    set --
    
    # Displays 0 (as earlier)
    echo $#

    选项 --(两个连字符)表示选项的结束,并意味着其后的任何内容都必须是命令参数。

  • 命令替换:您可以使用 $(commands) 的形式来捕获这些命令的输出,用于其他命令或变量,而不是使用反引号。这使得引用和转义更加容易。
  • 变量替换:您可以将这些替换与命令参数(由 bash 为 shell 脚本创建)或您创建的变量一起使用。
    替换 Effect
    ${var1:-var2} 如果 var1null 或不存在,则使用 var2
    ${var1:=var2} 如果 var1null 或不存在,则使用 var2 的值并将其设置为 var1
    ${var1:?msg} 如果 var1null 或不存在,则将 msg 显示为错误
    ${var1:+var2} 如果 var1 存在,则使用 var2,但不将其设置为 var1
    ${var:offset} var 中从 offset 个字符之后的所有内容
    ${var:offset:length} var 中从 offset 个字符之后,长度为 length 的字符
    ${!prefix*} ${!prefix@} 所有以 prefix 开头的变量名
    ${!var[@]} ${!var[*]} 数组变量 var 的所有索引
    ${#var} var 的值长度
    ${var#drop} var 的值,不包含与正则表达式模式 drop 匹配的前缀
    ${var##drop} 如果前缀与正则表达式模式 drop 匹配,则为空字符串
    ${var%drop} var 的值,不包含与正则表达式模式 drop 匹配的后缀
    ${var%%drop} 如果后缀与正则表达式模式 drop 匹配,则为空字符串
    ${var^letter} 如果 var 的第一个字母匹配 letter(任何字母、* 或 ?),则将其更改为大写
    如果未指定 letter,则 var 的所有第一个字母将更改为大写
    ${var^^letter} 如果 var 中的任何字母匹配 letter(任何字母、* 或 ?),则将其更改为大写
    如果未指定 letter,则 var 的所有字母将更改为大写
    ${var,letter} 如果 var 的第一个字母匹配 letter(任何字母、* 或 ?),则将其更改为小写
    如果未指定 letter,则 var 的所有第一个字母将更改为小写
    ${var,,letter} 如果 var 中的任何字母匹配 letter(任何字母、* 或 ?),则将其更改为小写
    如果未指定 letter,则 var 的所有字母将更改为小写
    ${var/find/replace} var 的值,其中 find 的实例被替换为 replace。如果 find 以“#”开头,则在开头进行匹配。以“%”开头则匹配结尾。

转义

您可以使用反斜杠(\)转义

  • 特殊字符。要转义反斜杠字符,请使用双反斜杠(\\)。
  • 通过将字面文本字符串用单引号(' ')括起来。Bash 不会执行任何展开或替换。单引号括起来的字符串不应包含更多单引号。Bash 也不会执行反斜杠转义。
  • 通过将字面文本字符串用双引号(" ")括起来,但允许
    • $ 前缀的变量、展开和替换
    • 反斜杠转义的字符
    • 反引号(` `)命令字符串
    • 历史扩展字符
# Displays Hello World
a=World; echo "Hello $a"

# Displays Hello $a
a=World; echo 'Hello $a'

# Displays Hello 'World'
a=World; echo "Hello '$a'" 

印刷错误

在几个地方,Bash 参考手册(或者甚至本文)使用了错误的引号字符。在单引号字符串中使用的撇号u+0027 可能会被替换为右单引号u+2019。在反引号字符串中使用的重音符或 u+0060 可能会被替换为左单引号u+2018。在双引号字符串中使用的引号u+0022 也可能被替换为左右双引号。它们看起来相似,但在 shell 脚本或命令行中使用时会导致错误。我用 CommonMark(一种标准化的 MarkDown 方言,我写了第一本关于它的书)撰写书籍和文章,并将其输出为 HTML、ODT、EPUB 和 PDF 文档。这些文档不会出现这些引号错误。当有人在打印前在富文本编辑器或页面布局软件(如 LibreOffice Writer、Microsoft Word 或 Adobe Indesign)中编辑文档时,该程序的自动更正功能会用反引号替换普通引号和重音符。只需注意这种情况可能发生。为避免错误,请手动输入命令,不要复制粘贴。

摘要

我相信您也会得出结论,bash 代码可能非常晦涩。大量的生产代码(工业级 shell 脚本)有数百行长。如果 bash 不那么简洁强大,编写这些代码将花费永恒的时间。如果您要进行任何严肃的 shell 脚本编写,那么最好了解 bash 的各种秘密。我认为我已经涵盖了足够多的内容来激发您的兴趣。您现在可以自己探索了。

注释

  • 本文最初发表于 2022 年的 Open Source For You 杂志。我于 2023 年在 CodeProject 上重新发布了它。
  • 本文来源于我的书籍 Linux Command-Line Tips & Tricks。该书在许多电子书商店均可免费获取。
  • 我关于标准化 MarkDown 方言CommonMark Ready Reference的书籍,在许多电子书商店也可以免费获取。如果您编写与编码相关的文章或书籍(希望使用 CommonMark),请在您的编辑/排版/出版商处添加一条注释,要求他们首先禁用其软件中的自动更正功能。

历史

  • 2023 年 3 月 6 日:初始版本
© . All rights reserved.