原文prithu.dev/posts/unix-… 由二川翻译
UNIX 哲学强调构建简单且可扩展的软件。每个软件必须做一件事,并且做好这件事。而且,软件应该能够通过共同的接口——文本流与其他程序一起工作。这是UNIX的核心哲学之一,这使其如此强大和直观易用。
尽管 UNIX 系统引入了许多创新的程序和技术,但没有单一的程序或想法能使其良好运行。相反,使其有效的是编程方法,一种关于使用计算机的哲学。虽然这种哲学无法用一句话来概括,但其核心思想是:系统的力量来自于程序之间的关系,而不是单个程序本身。许多 UNIX 程序在单独运行时做的事情相当琐碎,但与其他程序结合起来,就成为通用而有用的工具。-- 《UNIX编程环境》
我认为这解释得很清楚了。同时,可以观看 Brian Kernighan 的视频,他非常厉害地解释了 UNIX 操作系统的基础知识,并演示了使用管道的例子。
不过,我想在这篇文章中展示这种哲学落实的一些例子——展示如何使用不同的UNIX工具结合起来完成一些强大的任务。
示例:
- 打印一个作者排行榜,根据他们在 Git 仓库中提交的提交数来排序
- 从/r/memes 浏览表情包,并从/r/earthporn 设置您的桌面壁纸
- 从 IMDb 列表中获取随机电影
示例 1 - 根据 Git 仓库中提交的提交数打印作者排行榜
让我们从简单的例子开始——显示一个Git仓库的作者/贡献者列表,并根据提交的数量对该列表进行排序,按降序排列(最多提交的作者排在最前面)。从管道的角度来看,这是一个简单的任务。使用 git log 命令可以显示提交日志,我们可以通过传递 --format=来指定提交的显示格式。 --format='%an' 只打印每个提交的作者姓名。
$ git log --format='%an'
Alice
Bob
Denise
Denise
Candice
Denise
Alice
Alice
Alice
现在我们可以使用 sort 工具按字母顺序对它们进行排序。
$ git log --format='%an' | sort
Alice
Alice
Alice
Alice
Bob
Candice
Denise
Denise
Denise
接下来我们使用 uniq 工具。
$ git log --format='%an' | sort | uniq -c
4 Alice
1 Bob
1 Candice
3 Denise
根据 uniq 命令的 man 手册:
uniq - 报告或省略重复的行 从输入(或标准输入)中过滤相邻的匹配行,写入输出(或标准输出)。
因此,uniq 会打印重复的行,但仅限于相邻的重复行。这就是为什么我们必须首先将输出传递给 sort 的原因。 -c 显示每行出现的次数。
你可以看到输出仍然按字母顺序排序。现在剩下的事情就是对它进行按数字排序。在 sort 中可以使用-n实现按数字排序。
$ git log --format='%an' | sort | uniq -c | sort -nr
4 Alice
3 Denise
1 Candice
1 Bob
-r 实现倒序排序。这样就实现了这个需求——按提交数量排序的作者列表。
示例 2 - 从/r/memes 浏览表情包,并从/r/earthporn 设置您的桌面壁纸
你知道吗,你可以在 reddit 的 URL 后面添加 ".json" 来获取 JSON 响应,来替代通常长的html响应。这开启了无限的可能性!其中之一就是可以直接从命令行浏览梗图(实际的图像将在 GUI 程序中显示)。我们可以简单地使用 curl 或 wget 命令获取 URL - reddit.com/r/memes.jso…
$ wget -O - -q 'https://reddit.com/r/memes.json'
'{"kind": "Listing", "data": {"modhash": "xyloiccqgm649f320569f4efb427cdcbd89e68aeceeda8fe1a", "dist": 27, "children":
[{"kind": "t3", "data": {"approved_at_utc": null, "subreddit": "memes",
"selftext": "More info available at....'
...
...
More lines
...
...
我在这里使用 wget,因为 Curl 的 User-Agent 会有所不同。显然,你可以通过更改 'User-Agent' 头部来解决这个问题,但我选择了 wget。wget 有一个 -O 选项,用于指定输出文件名。大多数接受此类选项的程序也允许将值设为 - ,它表示标准输出或输入,具体取决于上下文。 -q 选项只告诉 wget 保持安静,不打印诸如进度状态之类的东西。现在我们得到了一个大的 JSON 结构。为了在命令行上有意义地解析和使用这个 JSON 数据,我们可以使用 jq。jq 可以被视为 JSON 的 sed/awk,它有自己简单直观的语言,你可以参考它的手册。
如果你看一下响应 JSON,它看起来像这样:
{
"kind": "Listing",
"data": {
"modhash": "awe40m26lde06517c260e2071117e208f8c9b5b29e1da12bf7",
"dist": 27,
"children": [],
"after": "t3_gi892x",
"before": null
}
}
所以在这里,我们有一些类型为“Listing”的响应,我们可以看到我们有一个“children”数组。该数组的每个元素都是一个帖子。
这是“children”数组的一个元素的样子:
{
"kind": "t3",
"data": {
"subreddit": "memes",
"selftext": "",
"created": 1589309289,
"author_fullname": "t2_4amm4a5w",
"gilded": 0,
"title": "Its hard to argue with his assessment",
"subreddit_name_prefixed": "r/memes",
"downs": 0,
"hide_score": false,
"name": "t3_gi8wkj",
"quarantine": false,
"permalink": "/r/memes/comments/gi8wkj/its_hard_to_argue_with_his_assessment/",
"url": "https://i.redd.it/6vi05eobdby41.jpg",
"upvote_ratio": 0.93,
"subreddit_type": "public",
"ups": 11367,
"total_awards_received": 0,
"score": 11367,
"author_premium": false,
"thumbnail": "https://b.thumbs.redditmedia.com/QZt8_SBJDdKLVnXK8P4Wr_02ALEhGoGFEeNhpsyIfvw.jpg",
"gildings": {},
"post_hint": "image",
".................."
"more lines skipped"
".................."
}
}
我已经减少了 data 中键值对的数量。总共有 105 个项目。正如你所看到的,你可以获取许多有趣的关于帖子的数据属性。我们感兴趣的是帖子的 url。这不是实际 reddit 帖子的 url,而是帖子内容的 rul。如果您想要帖子 URL,那么就是 permalink。因此,在这种情况下,url 字段是梗图的图像 url。
我们可以简单地使用以下命令获取每个帖子的所有 url 列表:
$ wget -O - -q reddit.com/r/memes.json | jq '.data.children[] |.data.url'
"https://www.reddit.com/r/memes/comments/g9w9bv/join_the_unofficial_redditmc_minecraft_server_at/"
"https://www.reddit.com/r/memes/comments/ggsomm/10_million_subscriber_event/"
"https://i.imgur.com/KpwIuSO.png"
"https://i.redd.it/ey1f7ksrtay41.jpg"
"https://i.redd.it/is3cckgbeby41.png"
"https://i.redd.it/4pfwbtqsaby41.jpg"
...
...
忽略前两个链接,它们基本上是版主放置的置顶帖子,它们的“url”与“permalink”相同。
jq 从标准输入读取数据,并且它的输入是我们之前看到的 JSON 数据。.data.children 是指我之前提到的帖子数组。- .data.children[] | .data.url 的意思是,“迭代数组中的每个元素,并打印每个元素的‘data’字段中的‘url’字段”。
因此,我们得到了 /r/memes 的“热门”帖子的所有 URL 列表。如果您想获取本周的“热门”帖子,则可以访问 reddit.com/r/memes/top…。 想要获取所有时间的“热门”帖子?t=all,获取一年?t=year,等等。
一旦我们有了所有 URL 的列表,我们现在可以把它直接传递给 xargs。xargs 是一个非常有用的实用程序,可以从标准输入中构建命令行。这是 xargs 手册中的说明:
xargs 从标准输入中读取项目,这些项目由空格(可以用双引号、单引号或反斜杠保护)或换行符分隔,并使用任何初始参数一次或多次执行命令(默认为 /bin/echo),后面跟着从标准输入读取的项目。标准输入中的空白行将被忽略。
因此,运行以下命令之类的命令:
$ echo "https://i.redd.it/4pfwbtqsaby41.jpg" | xargs wget -O meme.jpg -q
等价于运行以下命令:
$ wget -O meme.jpg -q "https://i.redd.it/4pfwbtqsaby41.jpg"
现在,我们只需将 URL 列表传递给图像查看器,如 feh 或 eog,它们接受 URL 作为有效参数。
$ wget -O - -q reddit.com/r/memes.json | jq '.data.children[] |.data.url' | xargs feh
现在,feh 弹出了梗图,我可以使用箭头键浏览它们,就像它们在我的本地磁盘上一样。
Feh screen
或者我可以使用 wget 下载所有图像,只需将上面的 feh 替换为 wget。
这种 reddit JSON 数据的另一个用途是将您的桌面壁纸设置为 /r/earthporn 的“热门”部分中被投票最多的图片。这些可能性是无穷无尽的。
$ wget -O - -q reddit.com/r/earthporn.json | jq '.data.children[] |.data.url' | head -1 | xargs feh --bg-fill
如果您愿意,可以将其设置为每小时运行的 cron 作业。我在这里使用 head 命令只打印第一行,即获得最多赞的帖子。head 看起来似乎非常微不足道,毫无用处,但在这种情况下,与其他程序一起使用,它成为了一个重要的部分。 您看到了 UNIX 管道的威力了吗?这一行命令从获取 JSON 数据、解析并从中获取相关数据,然后从 URL 获取图像,最后将其设置为壁纸,所有这些都可以实现。
我使用这个东西每两个小时从 /r/memes 下载梗图。这已经设置为我的机器上的 cron 作业。现在我有大约 19566 个梗图,在我的硬盘上占用了 4.5G。为什么我要这样做?别问我...
示例 3 - 从 IMDb 列表中获取随机电影
让我们以一个简单的示例结束。IMDb 有一个功能,允许您制作列表。您还可以找到其他用户创建的列表。例如 - Blow Your Mind Movies。如果将 /export 添加到 URL 中,则可以获取以 .csv 格式呈现的列表。
$ curl https://www.imdb.com/list/ls020046354/export
Position,Const,Created,Modified,Description,Title,URL,Title Type,IMDb Rating,Runtime (mins),Year,Genres,Num Votes,Release Date,Directors
1,tt0137523,2017-07-30,2017-07-30,,Fight Club,https://www.imdb.com/title/tt0137523/,movie,8.8,139,1999,Drama,1780706,1999-09-10,David Fincher
2,tt0945513,2017-07-30,2017-07-30,,Source Code,https://www.imdb.com/title/tt0945513/,movie,7.5,93,2011,"Action, Drama, Mystery, Sci-Fi, Thriller",471234,2011-03-11,Duncan Jones
3,tt0482571,2017-07-30,2017-07-30,,The Prestige,https://www.imdb.com/title/tt0482571/,movie,8.5,130,2006,"Drama, Mystery, Sci-Fi, Thriller",1133548,2006-10-17,Christopher Nolan
4,tt0209144,2018-01-16,2018-01-16,,Memento,https://www.imdb.com/title/tt0209144/,movie,8.4,113,2000,"Mystery, Thriller",1081848,2000-09-05,Christopher Nolan
5,tt0144084,2018-01-16,2018-01-16,,American Psycho,https://www.imdb.com/title/tt0144084/,movie,7.6,101,2000,"Comedy, Crime, Drama",462984,2000-01-21,Mary Harron
6,tt0364569,2018-01-16,2018-01-16,,Oldeuboi,https://www.imdb.com/title/tt0364569/,movie,8.4,120,2003,"Action, Drama, Mystery, Thriller",491476,2003-11-21,Chan-wook Park
7,tt1130884,2018-10-08,2018-10-08,,Shutter Island,https://www.imdb.com/title/tt1130884/,movie,8.1,138,2010,"Mystery, Thriller",1075524,2010-02-13,Martin Scorsese
8,tt8772262,2019-12-27,2019-12-27,,Midsommar,https://www.imdb.com/title/tt8772262/,movie,7.1,148,2019,"Drama, Horror, Mystery, Thriller",150798,2019-06-24,Ari Aster
我们可以使用 cut 命令来确定需要打印哪些字段:
$ curl https://www.imdb.com/list/ls020046354/export | cut -d ',' -f 6
Title
Fight Club
Source Code
The Prestige
Memento
American Psycho
Oldeuboi
Shutter Island
Midsommar
-d 选项用于指定每个字段的分隔符。在这种情况下,它是逗号(,)。-f 选项是您要打印的字段编号。在这种情况下,第六个字段是电影的标题。这还打印了 csv 标头“Title”,因此要删除它,我们可以使用 sed '1 d',它的意思是从输入流中删除 1 行。
然后我们可以将电影列表传输到 shuf。Shuf 只是随机地打乱其输入行并输出它。
$ curl https://www.imdb.com/list/ls020046354/export | cut -d ',' -f 6 | sed '1 d' | shuf
American Psycho
Midsommar
Source Code
Oldeuboi
Fight Club
Memento
Shutter Island
The Prestige
现在只需将其传输到 head -1 或 sed '1 q',它将仅打印第一行。每次运行此命令,您应该会得到一个随机选择。
$ curl https://www.imdb.com/list/ls020046354/export | cut -d ',' -f 6 | sed '1 d' | shuf | head -1
Source Code
现在假设您还想要将 URL 与标题一起打印,没问题,cut 允许您使用 --field=LIST 指定要打印的多个字段。
$ curl https://www.imdb.com/list/ls020046354/export | cut -d ',' --field=6,7 | sed '1 d' | shuf | head -1
Shutter Island,https://www.imdb.com/title/tt1130884/
不过,这里有一个问题,如果电影标题中包含逗号,则会获得完全不同的字段值。解决这个问题的一种方法是使用像这样的 Python 单行代码:
python -c 'import csv,sys;[print (a["Title"]) for a in csv.DictReader(sys.stdin)]'
$ curl -s https://www.imdb.com/list/ls020046354/export |\
python -c 'import csv,sys;[print (a["Title"],a["URL"]) for a in csv.DictReader(sys.stdin)]'|\
shuf | head -1
Oldeuboi https://www.imdb.com/title/tt0364569/
这只是一些例子,使用管道可以在一行 shell 命令中完成很多事情。