Unix管道:让命令行更简洁高效

avatar

原文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-meme.png

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 命令中完成很多事情。