MIT2020补习班——(二)Shell工具和脚本

Shell脚本

要在bash中分配变量,请使用foo=bar语法并使用$foo访问变量的值。注意foo = bar不起作用,因为它被解释为用参数 = 和 bar 调用foo程序。一般来说,在shell脚本中,空格字符将执行参数拆分,并且在开始使用时可能会很混乱,因此请始终检查该选项。

bash中的字符串可以用'分隔符定义,但它们不等价。用'分隔的字符串是文本字符串,不会替换变量值,而用分隔的字符串会。

foo=bar
echo "$foo"
# prints bar
echo '$foo'
# prints $foo

与大多数编程语言一样,bash支持控制流技术,包括ifcasewhilefor。类似地,bash具有接受参数并可以对其进行操作的函数。下面是一个函数的示例,该函数创建一个目录并cd进入其中。

mcd () {
    mkdir -p "$1"
    cd "$1"
}

这里$1是脚本/函数的第一个参数。与其他脚本语言不同,bash使用各种特殊变量来引用参数、错误代码和其他相关变量。

$0-脚本的名称

$1$9—脚本的参数。$1是第一个参数等等。

$@-所有参数

$#-参数的个数

$? -上一个命令的返回代码

$$-当前脚本的进程标识号

!! -整个最后一个命令,包括参数。一个常见的模式是只执行一个由于缺少权限而失败的命令,然后您可以通过执行sudo来使用sudo!!快速执行它

$_-最后一个命令的最后一个参数。如果您在一个交互式shell中,您还可以通过键入Esc并后跟.来快速获取此值。

命令通常使用STDOUT返回输出,通过STDERR返回错误,并返回代码以更友好的脚本方式报告错误。返回代码或退出状态是脚本/命令必须通信执行方式的方式。值为0通常表示一切正常,与0不同的值表示发生错误。

退出代码可用于使用&&(和运算符)和||(或运算符)有条件地执行命令。命令也可以用分号;分隔在同一行中。true程序将始终具有0返回代码,false命令将始终具有1返回代码。让我们看看一些例子

false || echo "Oops, fail"
# Oops, fail

true || echo "Will not be printed"
#

true && echo "Things went well"
# Things went well

false && echo "Will not be printed"
#

false ; echo "This will always run"
# This will always run

另一个常见的模式是希望将命令的输出作为变量。这可以通过命令替换来完成。每当您放置$(CMD)时,它将执行CMD,获取命令的输出并将其替换到位。例如,如果对$(ls)中的文件执行此操作,shell将首先调用ls,然后对这些值进行迭代。一个鲜为人知的类似特性是进程替换,<(CMD)将执行CMD并将输出放在一个临时文件中,并用该文件的名称替换<()。当命令期望值由文件传递而不是由STDIN传递时,这是有用的。例如,diff<(ls foo)<(ls bar)将显示 foobar中文件之间的差异。

因为这是一个巨大的信息转储,让我们看一个展示这些特性的例子。它将遍历我们提供的参数,字符串foobargrep,如果找不到它,则将其作为注释追加到文件中。

#!/bin/bash

echo "Starting program at $(date)" # Date will be substituted

echo "Running program $0 with $# arguments with pid $$"

for file in $@; do
    grep foobar $file > /dev/null 2> /dev/null
    # When pattern is not found, grep has exit status 1
    # We redirect STDOUT and STDERR to a null register since we do not care about them
    if [[ $? -ne 0 ]]; then
        echo "File $file does not have any foobar, adding one"
        echo "# foobar" >> "$file"
    fi
done

在比较中,我们测试了$?是否不等于0。Bash实现了许多此类比较,当在Bash中执行比较时,可以在手册页中找到一个详细的列表,尝试使用双括号[[]]而不是简单的括号[]

启动脚本时,通常需要提供类似的参数。Bash可以通过执行文件名扩展来简化这个过程,扩展表达式。这些技术通常被称为贝壳球。

通配符-每当你想执行某种通配符匹配,你可以使用*分别匹配一个或任意数量的字符。例如,给定文件foofoo1foo2foo10bar,命令rm foo?将删除foo1foo2,而rm foo*将删除除bar之外的所有内容。

花括号{}-只要在一系列命令中有一个公共子字符串,就可以使用bash的花括号来自动扩展它。这在移动或转换文件时非常方便。

convert image.{png,jpg}
# Will expand to
convert image.png image.jpg

cp /path/to/project/{foo,bar,baz}.sh /newpath
# Will expand to
cp /path/to/project/foo.sh /path/to/project/bar.sh /path/to/project/baz.sh /newpath

# Globbing techniques can also be combined
mv *{.py,.sh} folder
# Will move all *.py and *.sh files


mkdir foo bar
# This creates files foo/a, foo/b, ... foo/j, bar/a, bar/b, ... bar/j
touch {foo,bar}/{a..j}
touch foo/x bar/y
# Show differences between files in foo and bar
diff <(ls foo) <(ls bar)
# Outputs
# < x
# ---
# > y

在这里插入图片描述

请注意,不一定要用bash编写脚本才能从终端调用。例如,这里有一个简单的Python脚本,它以相反的顺序输出参数

#!/usr/local/bin/python
import sys
for arg in reversed(sys.argv[1:]):
    print(arg)

shell知道使用python解释器而不是shell命令来执行这个脚本,因为我们在脚本的顶部包含了一个shebang行。使用env命令编写shebang行是一种很好的实践,该命令将解析到系统中命令所在的任何位置,从而提高脚本的可移植性。为了解决位置问题,env将使用我们在第一堂课中介绍的PATH环境变量。在这个例子中,shebang线看起来像#!/usr/bin/env python

shell函数和脚本之间的一些区别应该记住:

  • 函数必须使用与shell相同的语言,而脚本可以使用任何语言编写。这就是为什么包含脚本的shebang很重要的原因。

  • 函数在读取其定义时加载一次。每次执行脚本时都会加载它们。这使得加载函数的速度稍快,但每当您更改它们时,都必须重新加载它们的定义。

  • 函数在当前shell环境中执行,而脚本在其自己的进程中执行。因此,函数可以修改环境变量,例如,更改当前目录,而脚本不能。脚本将通过使用export导出的值环境变量传递

  • 与任何编程语言一样,函数是实现模块化、代码重用和外壳代码清晰性的强大构造。通常shell脚本将包含它们自己的函数定义。

Shell工具

查找如何使用命令

此时,您可能想知道如何在别名部分(如ls -lmv -imkdir -p)中找到命令的标志。更一般地,给定一个命令,如何找出它的作用及其不同的选项?

正如我们在shell课程中看到的,第一个方法是使用-h--help标志调用所述命令。更详细的方法是使用man命令。man是manual的缩写,它为您指定的命令提供了一个manual页面(称为manpage)。例如,man rm将输出rm命令的行为以及它所使用的标志,包括前面显示的-i标志。即使是您安装的非本机命令,如果开发人员编写了manpage条目并将其作为安装过程的一部分包含进来,也会有manpage条目。对于交互式工具(如基于ncurses的工具),通常可以在程序中使用:help命令或键入

有时,manpages可能是对命令的过于详细的描述,并且很难破译用于常见用例的标志/语法。TLDR页面是一个很好的补充解决方案,它侧重于给出命令的示例用例,这样您就可以快速找出要使用的选项。

查找文件

每个程序员面临的最常见的重复任务之一是查找文件或目录。所有类UNIX系统都附带find,这是一个查找文件的好shell工具。find将递归搜索符合某些条件的文件。一些例子:

# Find all directories named src
find . -name src -type d
# Find all python files that have a folder named test in their path
find . -path '**/test/**/*.py' -type f
# Find all files modified in the last day
find . -mtime -1
# Find all zip files with size in range 500k to 10M
find . -size +500k -size -10M -name '*.tar.gz'

除了列出文件外,find还可以对与查询匹配的文件执行操作。此属性对于简化可能相当单调的任务非常有用。

# Delete all files with .tmp extension
find . -name '*.tmp' -exec rm {} \;
# Find all PNG files and convert them to JPG
find . -name '*.png' -exec convert {} {.}.jpg \;

尽管find无处不在,但它的语法有时很难记住。例如,要简单地查找与某个模式匹配的文件,必须执行find -name '*PATTERN*'(如果希望模式匹配不区分大小写,则执行-iname)。您可以开始为这些场景构建别名,但作为shell哲学的一部分,使用替代方法进行探索是很好的。记住,shell最好的特性之一是,您只需调用程序,就可以找到(甚至自己编写)某些程序的替换项。例如,fd是一种简单、快速和用户友好的查找方法。它提供了一些不错的默认设置,如彩色输出、默认正则表达式匹配、Unicode支持,而且它在我看来是一种更直观的语法。查找PATTERN模式的语法是fd PATTERN

大多数人都同意findfd是很好的,但是有些人可能想知道每次查找文件的效率,而不是编译某种索引或数据库以便快速搜索。这就是locate的目的。locate使用使用updatedb更新的数据库。在大多数系统中,updatedb是通过cron每天更新的。因此,两者之间的一个权衡是速度与新鲜度。此外,find和类似工具还可以使用文件大小、修改时间或文件权限等属性查找文件,而locate只使用名称。

查找代码

查找文件是很有用的,但通常是在查找文件中的内容。一个常见的场景是希望搜索包含某个模式的所有文件,以及这些文件中出现该模式的位置。为了实现这一点,大多数类UNIX系统都提供了grep,一个用于从输入文本中匹配模式的通用工具。

grep有许多标志,使其成为一个非常通用的工具。我经常使用-C来获取匹配行周围的上下文,使用-v来反转匹配,即打印所有与模式不匹配的行。例如,grep -C 5将在匹配前后打印5行。当涉及到快速解析许多文件时,您需要使用-R,因为它将递归地进入目录并查找匹配字符串的文本文件。

但是grep -R可以在很多方面得到改进,比如忽略.git文件夹,使用多CPU支持,&c。因此开发的替代方案并不缺乏,包括ack、ag和rg。它们都很棒,但基本上满足了同样的需求。现在我坚持使用ripgreprg),考虑到它的速度和直观性。一些例子:

# Find all python files where I used the requests library
rg -t py 'import requests'
# Find all files (including hidden files) without a shebang line
rg -u --files-without-match "^#!"
# Find all matches of foo and print the following 5 lines
rg foo -A 5
# Print statistics of matches (# of matched lines and files )
rg --stats PATTERN

请注意,与find/fd一样,重要的是您知道可以使用这些工具之一快速解决这些问题,而您使用的特定工具则不那么重要。

查找shell命令

到目前为止,我们已经了解了如何查找文件和代码,但是随着您开始在shell中花费更多时间,您可能希望在某个时候查找您键入的特定命令。首先要知道的是,向上键将返回上一个命令,如果你一直按它,你将慢慢地浏览你的shell历史。

history命令将允许您以编程方式访问shell历史记录。它将把您的shell历史记录打印到标准输出。如果我们想在那里搜索,我们可以将输出管道发送到grep并搜索模式。history | grep find将打印带有子字符串“find”的命令。

在大多数shell中,您可以使用Ctrl+R在历史记录中执行向后搜索。按Ctrl+R后,可以键入要与历史记录中的命令匹配的子字符串。当你不断按它,你将循环通过你的历史上的输入。这也可以通过zsh中的向上/向下箭头启用。除了Ctrl+R之外,还有一个很好的附加功能,就是使用fzf绑定。fzf是一个通用的模糊查找器,可以与许多命令一起使用。

基于历史的自我暗示history-based autosuggestions。此功能首先由fish shell引入,它使用您键入的最新命令动态地自动完成当前shell命令,该命令与当前shell命令共享一个公共前缀。

最后,需要记住的一点是,如果使用前导空格启动命令,它不会添加到shell历史记录中。当您使用密码或其他敏感信息输入命令时,这非常有用。如果您犯了不添加前导空格的错误,您可以通过编辑.bashúhistory.zhistory来手动删除条目。

目录导航

到目前为止,我们假设您已经在需要执行这些操作的地方了,但是如何快速导航目录呢?有很多简单的方法可以做到这一点,比如编写shell别名,用ln-s创建符号链接,但事实是,开发人员现在已经找到了相当聪明和复杂的解决方案。

与本课程的主题一样,您通常希望针对常见情况进行优化。查找频繁和/或最近使用的文件和目录可以通过fasd fasd等工具按频率(即按频率和最近使用频率)对文件和目录进行排序。最直接的用法是autojump,它添加了一个z命令,可以使用frecent目录的子字符串快速cd。E、 g.如果你经常去/home/user/files/cool_project,你可以简单地跳到那里。

存在更复杂的工具来快速获取目录结构树、BROOT或甚至成熟的文件管理器(如NNN或Ranger-Rever)的概述。

发布了28 篇原创文章 · 获赞 2 · 访问量 3259

猜你喜欢

转载自blog.csdn.net/Maestro_T/article/details/104325031