使用 Bash 开发控制台应用程序

作者:Andy Carlson

将 Linux 命令行的强大功能带入您的应用程序开发流程中。

作为一名新手软件开发人员,我在选择编程语言时首先关注的是:是否有库允许我与系统接口以完成任务? 如果 Python 没有 Flask,我可能会选择另一种语言来编写 Web 应用程序。 出于同样的原因,我开始使用 Bash 开发许多(诚然很小的)应用程序。 尽管例如 Python 有许多模块可以导入和扩展功能,但 Bash 有数千个命令可以执行各种功能,包括字符串操作、数学计算、加密和数据库操作。 在本文中,我将介绍这些功能以及如何在 Bash 应用程序中轻松使用它们。

可重用代码片段

Bash 提供了三个我认为在创建可重用函数时特别有用的功能:别名、函数和命令替换。 别名是长命令的命令行快捷方式。 这是一个例子


alias getloadavg='cat /proc/loadavg'

此示例的别名是 getloadavg。 定义后,它可以像任何其他 Linux 命令一样执行。 在这种情况下,alias 将转储 /proc/loadavg 文件的内容。 需要记住的是,这是一个静态命令别名。 无论执行多少次,它始终会转储相同文件的内容。 如果需要更改命令的执行方式(例如,通过传递参数),您可以创建一个函数。 Bash 中的函数与任何其他语言中的函数的功能相同:评估参数,并执行函数中的命令。 这是一个示例函数


getfilecontent() {
    if [ -f $1 ]; then
        cat $1
    else
        echo "usage: getfilecontent <filename>"
    fi
}

此函数声明将函数名称定义为 getfilecontentif/else 语句检查作为第一个函数参数 ($1) 指定的文件是否存在。 如果存在,则输出文件内容。 如果不存在,则显示用法文本。 由于合并了参数,此函数的输出将根据提供的参数而变化。

我要介绍的最后一个功能是命令替换。 这是一种重新分配命令输出的机制。 由于此功能的多功能性,让我们看两个示例。 这个示例涉及将输出重新分配给变量


LOADAVG="$(cat /proc/loadavg)"

命令替换的语法是 $(command),其中“command”是要执行的命令。 在此示例中,LOADAVG 变量将存储 /proc/loadavg 文件的内容。 此时,可以评估、操作变量,或者只是将其回显到控制台。

文本处理

如果说 UNIX 上的脚本编写与其他环境有什么不同,那就是其强大的文本处理能力。 尽管在 Linux 中编写脚本时可以使用许多文本处理机制,但这里我将介绍 grepawksed 和基于变量的操作。 grep 命令允许在文本中搜索,无论是在文件中还是从另一个命令管道传输而来。 这是一个 grep 示例


alias searchdate='grep
 ↪"[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]"'

此处创建的别名将搜索数据中 YYYY-MM-DD 格式的日期。 与 grep 命令一样,文本可以作为管道数据提供,也可以作为命令后的文件路径提供。 如示例所示,grep 命令的搜索语法包括使用正则表达式(或 regex)。

当处理文本行以提取分隔字段时,awk 是最简单的工具。 您可以使用 awk 创建 /proc/loadavg 文件的详细输出


awk '{ printf("1-minute: %s\n5-minute: %s\n15-minute:
 ↪%s\n",$1,$2,$3); }' /proc/loadavg

为了本示例的目的,让我们检查 /proc/loadavg 文件的结构。 这是一个单行文件,通常有五个空格分隔的字段,尽管此示例仅使用前三个字段。 与 Bash 函数参数非常相似,awk 中的字段被引用为变量,变量名由它们在行中的位置命名($1 是第一个字段,依此类推)。 在此示例中,前三个字段被引用为 printf 语句的参数。 printf 语句将显示三行,每行都将包含数据描述和数据本身。 请注意,每个 %s 都被替换为 printf 函数的相应参数。

在 Linux 上用于文本处理的所有可用命令中,sed 可以被认为是文本处理的瑞士军刀。 与 grep 一样,sed 使用正则表达式。 我在这里要介绍的具体操作涉及正则表达式替换。 为了进行准确的比较,让我们使用 sed 重新创建之前的 awk 示例


sed 's/^\([0-9]\+\.[0-9]\+\) \([0-9]\+\.[0-9]\+\)
 ↪\([0-9]\+\.[0-9]\+\).*$/1-minute: \1\n5-minute:
 ↪\2\n15-minute: \3/g' /proc/loadavg

由于这是一个很长的示例,我将把它分成较小的部分。 正如我所提到的,此示例使用正则表达式替换,它遵循以下语法:s/search/replace/g。“s”开始定义替换语句。“search”值定义您要搜索的文本模式,“replace”值定义您要替换搜索值的内容。 末尾的“g”是一个标志,表示文件中的全局替换,并且是替换语句可用的众多标志之一。 此示例中的搜索模式是


^\([0-9]\+\.[0-9]\+\) \([0-9]\+\.[0-9]\+\)
 ↪\([0-9]\+\.[0-9]\+\).*$

字符串开头的插入符号 (^) 表示正在处理的文本行的开头,字符串末尾的美元符号 ($) 表示文本行的结尾。 在此示例中搜索四件事。 前三项是


\([0-9]\+\.[0-9]\+\)

整个字符串都用转义的括号括起来,这使得括号内的值可以在 replace 值中使用。 就像 grep 示例一样,[0-9] 将匹配单个数字字符。 当后跟转义的加号时,它将匹配一个或多个数字字符。 转义的点号将匹配单个点号。 当您将整个表达式放在一起时,您将得到十进制数字的模式。

搜索值中的第四项只是一个点号后跟一个星号。 点号将匹配任何字符,星号将匹配零个或多个其前面的任何内容。 示例的 replace 值是


1-minute: \1\n5-minute: \2\n15-minute: \3

这主要由纯文本组成; 但是,它包含四个独特的特殊项目。 有换行符,用斜杠“-/n”表示。 其他三项是斜杠后跟一个数字。 此数字对应于搜索值中括号括起来的模式。 斜杠-1 是括号中的第一个模式,斜杠-2 是第二个模式,依此类推。 此 sed 命令的输出将与之前的 awk 命令完全相同。

我要讨论的最后一个字符串操作机制涉及使用 Bash 变量来操作字符串。 尽管这远不如传统的正则表达式强大,但它提供了多种操作文本的方法。 以下是一些使用 Bash 变量的示例


MYTEXT="my example string"
echo "String Length:  ${#MYTEXT}"
echo "First 5 Characters: ${MYTEXT:0:5}"
echo "Remove \"example\": ${MYTEXT/ example/}"

名为 MYTEXT 的变量是此示例使用的示例字符串。 第一个 echo 命令显示如何确定字符串变量的长度。 第二个 echo 命令将返回字符串的前五个字符。 此子字符串语法涉及起始字符索引(在本例中为零)和子字符串的长度(在本例中为五)。 第三个 echo 命令删除单词“example”以及前导空格。

数学计算

尽管文本处理可能是 Bash 脚本的优势所在,但仍然需要进行数学运算。 可以使用 bcawk 或 Bash 算术扩展来评估基本数学问题。 bc 命令能够通过交互式控制台界面和管道输入来评估数学问题。 就本文而言,让我们看看评估管道数据。 考虑以下情况


pow() {
    if [ -z "$1" ]; then
        echo "usage: pow <base> <exponent>"
    else
        echo "$1^$2" | bc
    fi
}

此示例显示了如何创建 C++ 中 pow 函数的实现。 该函数需要两个参数。 函数的结果将是第一个数字的第二个数字次方。 "$1^$2" 的数学语句通过管道输入到 bc 命令进行计算。

尽管 awk 确实提供了进行基本数学计算的能力,但 awk 迭代文本行的能力使其在创建摘要数据时特别有用。 例如,如果您想计算文件夹中所有文件的总大小,您可以使用如下内容


foldersize() {
    if [ -d $1 ]; then
        ls -alRF $1/ | grep '^-' | awk 'BEGIN {tot=0} {
         ↪tot=tot+$5 } END { print tot }'
    else
        echo "$1: folder does not exist"
    fi
    }

此函数将对作为参数提供的文件夹下的所有条目执行递归长列表。 然后,它将搜索所有以破折号开头的行(这将选择所有文件)。 最后一步是使用 awk 迭代输出并计算所有文件的组合大小。

以下是 awk 语句的分解方式。 在管道数据的处理开始之前,BEGIN 块将名为 tot 的变量设置为零。 然后,对于每一行,执行下一个块。 此块会将每行中第五个字段的值(即文件大小)添加到 tot。 最后,在管道数据处理完毕后,END 块将打印 tot 的值。

执行基本数学运算的另一种方法是通过算术扩展。 这将采用与命令替换类似的视觉效果。 让我们使用算术扩展重写之前的示例


pow() {
    if [ -z "$1" ]; then
        echo "usage: pow <base> <exponent>"
    else
        echo "$[$1**$2]"
    fi
}

算术扩展的语法是 $[expression],其中 expression 是数学表达式。 请注意,此示例未使用插入符号运算符表示指数,而是使用了双星号。 尽管这种计算方法存在差异和局限性,但语法可能比将数据通过管道传输到 bc 命令更直观。

密码学

根据应用程序的需求,可能需要对数据执行加密操作。 如果需要哈希字符串、加密文件或对数据进行 base64 编码,则可以使用 openssl 命令完成所有这些操作。 尽管 openssl 提供了大量的密码、哈希算法和其他功能,但我在这里仅介绍一些。

第一个示例显示了如何使用 blowfish 密码加密文件


 $1.enc
    else
        echo "usage: bf-enc <file> <password>"
    fi
}

此函数需要两个参数:要加密的文件和用于加密的密码。 运行后,此脚本会生成一个文件,该文件的名称与您的原始文件相同,但文件扩展名为“enc”。

加密数据后,您需要一个函数来解密它。 这是解密函数


bf-dec() {
    if [ -f $1 ] && [ -n "$2" ]; then
        cat $1 | openssl enc -d -blowfish -pass pass:$2 >
         ↪${1%%.enc}
    else
        echo "usage: bf-dec <file> <password>"
    fi
}

解密函数的语法与加密函数几乎相同,只是添加了“-d”来解密管道数据,以及从解密文件名末尾删除“.enc”的语法。

openssl 提供的另一项功能是创建哈希值的能力。 尽管可以使用 openssl 哈希文件,但我将在此处重点介绍哈希字符串。 让我们创建一个函数来创建字符串的 MD5 哈希值


md5hash() {
    if [ -z "$1" ]; then
        echo "usage: md5hash <string>"
    else
        echo "$1" | openssl dgst -md5 | sed 's/^.*= //g'
    fi
}

此函数将采用提供给函数的字符串参数,并生成该字符串的 MD5 哈希值。 命令末尾的 sed 语句将剥离 openssl 放在命令输出开头的文本,以便函数返回的唯一文本是哈希值本身。

验证哈希值(而不是解密它)的方法是创建一个新哈希值并将其与旧哈希值进行比较。 如果哈希值匹配,则原始字符串将匹配。

我还想讨论创建数据的 base64 编码字符串的能力。 我发现此功能特别有用的一个应用是创建 HTTP 基本身份验证标头字符串(其中包含用户名:密码)。 这是一个完成此操作的函数


basicauth() {
    if [ -z "$1" ]; then
        echo "usage: basicauth <username>"
    else
        echo "$1:$(read -s -p "Enter password: " pass ;
         ↪echo $pass)" | openssl enc -base64
    fi
}

此函数将采用作为第一个函数参数提供的用户名和通过命令替换由用户输入提供的密码,并使用 openssl 对字符串进行 base64 编码。 然后可以将此字符串添加到 HTTP 授权标头字段。

数据库操作

应用程序的实用性取决于其背后的数据。 尽管有一些命令行工具可以与数据库服务器软件交互,但这里我重点介绍基于 SQLite 文件的数据库。 将应用程序从一台计算机移动到另一台计算机时,可能会遇到一个难题,即根据 SQLite 版本,可执行文件的名称可能不同(通常是 sqlitesqlite3)。 使用命令替换,您可以创建一种万无一失的调用 sqlite 的方法


$(ls /usr/bin/sqlite* | grep 'sqlite[0-9]*$' | head -n1)

这将返回系统上可用的 sqlite 可执行文件的完整文件路径。

考虑一个应用程序,它在首次执行时创建一个空数据库。 如果使用此语法调用 sqlite 二进制文件,则始终会使用该系统上正确版本的 sqlite 创建空数据库。

以下是如何创建一个新数据库,其中包含个人信息表的示例


$(ls /usr/bin/sqlite* | grep 'sqlite[0-9]*$' | head -n1) test.db
 ↪"CREATE TABLE people(fname text, lname text, age int)"

这将创建一个名为 test.db 的数据库文件,并将按所述创建 people 表。 相同的语法可用于执行 SQLite 提供的任何 SQL 操作,包括 SELECT、INSERT、DELETE、DROP 等。

本文仅略微触及了在 Linux 上开发控制台应用程序的可用命令的表面。 有许多很棒的资源可以用于学习更深入的脚本编写技术,无论是在 Bash、awk、sed 还是任何其他基于控制台的工具集中。 请参阅“资源”部分,获取更多有用的信息链接。

资源

Andy Carlson 在 IT 领域工作了 15 年,从事网络和服务器管理以及偶尔的编码工作。 他很庆幸选择了自己热爱、成长和学习的职业。 他目前与妻子、三个女儿和一个儿子住在俄亥俄州辛辛那提市。 他的家人目前正在国际上收养两个孩子。 他喜欢弹吉他、编码以及与家人和朋友共度时光。

加载 Disqus 评论