在本讲座中,我们将介绍使用bash作为脚本语言的一些基础知识,以及一些Shell工具,这些工具涵盖了您将在命令行中不断执行的一些最常见的任务。
Shell脚本
到目前为止,我们已经看到了如何在Shell中执行命令并将它们通过管道传输在一起。但是,在许多情况下,您将需要执行一系列命令并利用条件或循环等控制流表达式。
Shell脚本是复杂性的下一步。大多数外壳程序都有自己的脚本语言,其中包含变量,控制流及其语法。使Shell脚本与其他脚本编程语言不同的原因是,它经过优化以执行与Shell相关的任务。因此,创建命令管道,将结果保存到文件中以及从标准输入中读取都是Shell脚本中的原语,这比通用脚本语言更易于使用。在本节中,我们将重点介绍bash脚本,因为它是最常见的脚本。
要在bash中分配变量,请使用语法foo=bar
并通过访问变量的值$foo
。请注意,这foo = bar
将不起作用,因为它被解释为foo
使用参数=
和调用程序bar
。通常,在shell脚本中,空格字符将执行参数拆分。刚开始使用时,这种行为可能会令人困惑,因此请务必进行检查。
bash中的字符串可以使用'
和"
分隔符进行定义,但是它们不是等效的。以分隔的'
字符串是文字字符串,不会替代变量值,而以"
分隔的字符串代替。
foo=bar
echo "$foo"
# prints bar
echo '$foo'
# prints $foo
与大多数编程语言一样,bash支持流量控制技术,包括if
,case
,while
和for
。同样,bash
具有接受参数并可以对其进行操作的函数。这是一个创建目录并将其cd
放入目录的函数示例。
mcd () {
mkdir -p "$1"
cd "$1"
}
这$1
是脚本/函数的第一个参数。与其他脚本语言不同,bash使用各种特殊变量来引用参数,错误代码和其他相关变量。以下是其中一些列表。在这里可以找到更全面的列表。
$0
-脚本名称$1
到$9
-脚本变量的情况。$1
是第一个参数,依此类推。$@
-所有论点$#
-参数数量$?
-上一条命令的返回码$$
-当前脚本的进程标识号(PID)!!
-整个最后一个命令,包括参数。一种常见的模式是仅执行命令以使其由于缺少权限而失败。您可以通过使用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"
#
true ; echo "This will always run"
# This will always run
false ; echo "This will always run"
# This will always run
另一个常见的模式是希望将命令的输出作为变量。这可以通过命令替换来完成。每当您放置$( CMD )
它时CMD
,它都会执行,获取命令的输出并将其替换到位。例如,如果这样做for file in $(ls)
,shell将首先调用ls
,然后迭代这些值。鲜为人知的类似功能是进程替换,<( CMD )
它将执行CMD
输出并将其放置在临时文件中,并<()
用该文件的名称替换。当命令期望值通过文件而不是STDIN传递时,这很有用。例如,diff <(ls foo) <(ls bar)
将显示dirsfoo
和中的文件之间的差异 bar
。
由于这是一个巨大的信息转储,因此让我们看一个展示一些这些功能的示例。它将遍历我们为grep
字符串提供的参数,foobar
如果找不到,则将其作为注释附加到文件中。
#!/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进行了许多此类比较-您可以在的联机帮助页中找到有关的详细列表test
。在bash中进行比较时,请尝试使用双括号[[ ]]
,而不是简单的括号[ ]
。尽管不会轻易犯错,但犯错的几率较低sh
。在这里可以找到更详细的解释。
启动脚本时,您通常会希望提供相似的参数。Bash可以通过执行文件名扩展来简化此过程,扩展表达式。这些技术通常被称为壳通配符。
- 通配符-每当您要执行某种通配符匹配时,都可以分别使用
?
和*
匹配一个或任意数量的字符。例如,给定文件foo
,foo1
,foo2
,foo10
和bar
,该命令rm foo?
将删除foo1
和foo2
而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/h, bar/a, bar/b, ... bar/h
touch {foo,bar}/{a..h}
touch foo/x bar/y
# Show differences between files in foo and bar
diff <(ls foo) <(ls bar)
# Outputs
# < x
# ---
# > y
编写bash
脚本可能很棘手且不直观。像shellcheck这样的工具可以帮助您在sh / bash脚本中发现错误。
请注意,脚本不一定要用bash编写才能从终端调用。例如,这是一个简单的Python脚本,它以相反的顺序输出其参数:
#!/usr/local/bin/python
import sys
for arg in reversed(sys.argv[1:]):
print(arg)
内核知道使用python解释器而不是shell命令来执行此脚本,因为我们在脚本顶部包含了shebang行。优良作法是使用该env
命令编写shebang行,该命令将解析到该命令在系统中的任何位置,从而增加了脚本的可移植性。为了解析位置,env
将利用PATH
我们在第一堂课中介绍的环境变量。对于这个例子,shebang线看起来像#!/usr/bin/env python
。
您应牢记的shell函数和脚本之间的一些差异是:
- 函数必须与外壳使用相同的语言,而脚本可以用任何语言编写。这就是为什么包括脚本的shebang如此重要的原因。
- 读取函数定义后,函数将被加载一次。脚本在每次执行时都会加载。这使函数的加载速度稍快一些,但是无论何时更改它们,都必须重新加载其定义。
- 函数在当前的shell环境中执行,而脚本在其自身的进程中执行。因此,函数可以修改环境变量,例如,更改当前目录,而脚本则不能。脚本将由使用以下命令导出的值环境变量传递
export
- 与任何编程语言一样,函数是一种强大的构造,可以实现模块化,代码重用和Shell代码的清晰性。通常,shell脚本将包含其自己的函数定义。
外壳工具
查找如何使用命令
在这一点上,你可能想知道如何找到在混叠部分,如命令的参数ls -l
,mv -i
和mkdir -p
。更一般地说,给定一个命令,您如何找出它的作用及其不同选项?您总是可以开始使用Google搜索,但是由于UNIX早于StackOverflow,所以有内置的方式来获取此信息。
正如我们在Shell讲座中所看到的,一阶方法是使用-h
or--help
标志调用所述命令。更详细的方法是使用man
命令。手册的man
缩写,为您指定的命令提供手册页(称为手册页)。例如,man rm
将输出rm
命令的行为及其采取的标志,包括-i
我们之前显示的标志。实际上,到目前为止,我一直为每个命令链接的是该命令的Linux联机帮助页的在线版本。如果开发人员编写了手册页条目并将其包括在安装过程中,则即使您安装的非本机命令也将具有手册页条目。对于交互式工具(例如基于ncurses的工具),通常可以使用:help
命令或键入来在程序中访问命令的帮助?
。
有时,联机帮助页可能会提供有关命令的过分详细的描述,从而使您难以理解在常见用例中使用的标志/语法。 TLDR页面是一个很好的补充解决方案,专注于给出命令示例用例,以便您可以快速确定要使用的选项。举例来说,我发现自己回头参考tldr页tar
和ffmpeg
方式往往比联机帮助页。
查找文件
每个程序员面临的最常见的重复性任务之一就是查找文件或目录。所有类似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
它无处不在,但是它的语法有时仍很难记住。例如,要简单地找到与某些模式匹配的文件,PATTERN
就必须执行find -name '*PATTERN*'
(或者-iname
如果您希望模式匹配不区分大小写)。您可以开始为这些方案构建别名,但是Shell哲学的一部分是,探索替代方案是一件好事。请记住,shell的最佳特性之一就是您只是在调用程序,因此您可以找到(甚至自己编写)某些程序的替代程序。例如,fd
是的简单,快速和用户友好的替代方案find
。它提供了一些不错的默认值,例如彩色输出,默认正则表达式匹配和Unicode支持。在我看来,它还具有更直观的语法。例如,查找模式的语法PATTERN
是fd PATTERN
。
大多数人都同意这种说法find
,fd
并且很好,但是你们当中有些人可能想知道每次查找文件的效率与编译某种索引或数据库以进行快速搜索的效率。那locate
是为了什么。 locate
使用使用进行更新的数据库updatedb
。在大多数系统中,updatedb
每天都会通过进行更新cron
。因此,两者之间的权衡是速度与新鲜度。而且find
,类似的工具也可以使用诸如文件大小,修改时间或文件权限之类的属性来查找文件,而locate
仅使用文件名。在这里可以找到更深入的比较。
查找代码
通过名称查找文件很有用,但是通常您想根据文件内容进行搜索。一种常见情况是希望搜索包含某种模式的所有文件,以及这些模式在这些文件中的位置。为了实现这一点,大多数类似UNIX的系统都提供grep
了通用工具,用于匹配输入文本中的模式。 grep
是一个非常有价值的外壳工具,我们将在数据争夺讲座中详细介绍。
就目前而言,要知道它grep
具有许多标志,这使其成为了一种非常通用的工具。一些我经常使用的是-C
用于获取ç ontext各地的匹配线和-v
在v耳听比赛,即打印那些所有行不匹配的模式。例如,grep -C 5
将在比赛前后打印5行。当涉及到迅速通过许多文件搜索,你要使用-R
,因为它会[R ecursively进入目录,并寻找匹配的字符串的文件。
但是grep -R
可以通过多种方式进行改进,例如.git
使用多CPU支持&c忽略文件夹。grep
已经开发了许多替代方案,包括ack,ag和rg。它们都很棒,几乎提供了相同的功能。就目前而言rg
,鉴于它的快速性和直观性,我坚持使用ripgrep()。一些例子:
# 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
查找shell命令
到目前为止,我们已经看到了如何查找文件和代码,但是随着您开始在shell中花费更多的时间,您可能希望找到在某个时候键入的特定命令。首先要知道的是,键入向上箭头将使您返回上一个命令,如果继续按该命令,则将慢慢浏览外壳历史记录。
该history
命令将允许您以编程方式访问您的Shell历史记录。它将您的shell历史记录打印到标准输出中。如果要在此处搜索,我们可以将该输出传递给管道grep
并搜索模式。 history | grep find
将打印包含子字符串“ find”的命令。
在大多数shell中,您可以利用Ctrl+R
进行历史记录的向后搜索。按下之后Ctrl+R
,您可以键入要与历史记录中的命令匹配的子字符串。持续按下它,您将循环浏览历史记录中的比赛。也可以使用zsh中的UP / DOWN箭头启用它。Ctrl+R
使用fzf绑定附带了一个不错的补充。 fzf
是可以与许多命令一起使用的通用模糊查找器。在这里,它用于模糊地匹配您的历史记录,并以方便且直观的方式呈现结果。
我真正喜欢的另一个很酷的与历史有关的技巧是基于历史的自动建议。最初由fish shell引入,此功能会使用您键入的最新命令(与之共享一个公共前缀)动态自动完成当前的shell命令。可以在zsh中启用它,这对于您的shell是一种很棒的生活技巧。
最后,要记住的一点是,如果您以前导空格开头的命令不会被添加到您的Shell历史记录中。当您输入带有密码或其他敏感信息位的命令时,这非常方便。如果您犯了没有添加前导空格的错误,则始终可以通过编辑.bash_history
或来手动删除条目.zhistory
。
目录导航
到目前为止,我们已经假定您已经是执行这些操作所需的位置。但是,如何快速浏览目录呢?您可以通过许多简单的方法来执行此操作,例如编写shell别名或使用ln -s创建符号链接,但事实是,开发人员目前已经找到了非常聪明和复杂的解决方案。
与本课程的主题一样,您通常希望针对常见情况进行优化。可以通过诸如fasd
和之类的工具来查找频繁和/或最新的文件和目录autojump
。Fasd通过频率(即频率和新近度)对文件和目录进行排名。默认情况下,添加一个命令,您可以使用该命令快速使用最新目录的子字符串。例如,如果您经常去,您可以使用跳到那里。使用自动跳转,可以使用来完成目录的相同更改。fasd
z
cd
/home/user/files/cool_project
z coolj cool
更复杂的工具可以迅速得到一个目录结构的概述:tree
,broot
或甚至完全成熟的文件管理器一样nnn
或ranger
。
编写bash函数 marco
,polo
然后执行以下操作。每当执行marco
当前工作目录时,都应以某种方式保存当前目录,然后在执行时polo
,无论位于哪个目录中,polo
都应cd
返回执行的目录marco
。为了便于调试,您可以将代码编写到文件中,marco.sh
然后通过执行将定义(重新)加载到Shell中source marco.sh
。
假设您有一条很少失败的命令。为了对其进行调试,您需要捕获其输出,但是运行失败可能很耗时。编写一个bash脚本,该脚本将运行以下脚本,直到其失败,并将其标准输出和错误流捕获到文件中,并在最后打印所有内容。如果您还可以报告该脚本失败运行了多少运行,则可以加分。
#!/usr/bin/env bash
n=$(( RANDOM % 100 ))
if [[ n -eq 42 ]]; then
echo "Something went wrong"
>&2 echo "The error was using magic numbers"
exit 1
fi
echo "Everything went according to plan"
-
正如我们所涵盖的讲座
find
的-exec
可以超过我们正在搜索的文件执行操作非常强大。但是,如果我们想对所有文件做些什么,例如创建一个zip文件,该怎么办?到目前为止,您已经看到命令将从参数和STDIN两者中获取输入。在传递命令时,我们将STDOUT连接到STDIN,但是某些命令(例如,tar
从参数中获取输入)。为了消除这种断开连接,有xargs
一条命令将使用STDIN作为参数来执行命令。例如ls | xargs rm
将删除当前目录中的文件。您的任务是编写一个命令,该命令以递归方式查找文件夹中的所有HTML文件,并使用它们进行压缩。请注意,即使文件中有空格(提示:检查
-d
标志是否存在xargs
),您的命令也应该起作用。如果您使用的是macOS,请注意默认的BSD
find
与GNU coreutils中包含的BSD不同。您可以-print0
在find
和-0
上使用标志xargs
。作为macOS用户,您应该意识到macOS附带的命令行实用程序可能与GNU对应程序有所不同;如果愿意,可以使用brew安装GNU版本。 -
(高级)编写命令或脚本以递归查找目录中最近修改的文件。更一般而言,您可以按新近度列出所有文件吗?