为什么这个脚本的最后一行会卡住?
#!/usr/bin/env bash
trap 'rm -f numbers' EXIT
mkfifo numbers
decrement() {
while read -r number; do
echo "debug: $number" >&2
if (( number )); then
echo $(( --number ))
else
break
fi
done
}
echo 10 > numbers &
# Works: prints the debug line
decrement < numbers >> numbers
# Works: prints an infinite stream of 10's
cat numbers | tee numbers
# Fails: prints "debug: 10" and then gets stuck
cat numbers | decrement | tee numbers
下面是我最初写的问题,但其中包含很多不必要的细节。不过,我保留它是为了以防万一有人好奇我是如何遇到这个的。开始:
是否可以循环使用命名管道/fifo?像这样的东西:
line → fifo ←───────┐
│ │
↓ ↑
│ │
curl ─────→ tee → stdout
这是我必须解决的问题。我想编写一个 Bash 实用程序来使用 Docker Hub API 获取 Docker 映像的所有标签。基本要求是这样的:
declare -r repo=library%2Fubuntu # %2F is a URL-encoded forward slash
curl "https://hub.docker.com/v2/repositories/$repo/tags/?page=1&page_size=100"
您会注意到,如果图像标签的总数大于每页请求的项目数(上限为 100),响应将包含指向下一页的链接。此外,该next
字段设置为null
最后一页上的时间。
{
"count": 447,
"next": "https://hub.docker.com/v2/repositories/library%2Fubuntu/tags/?page=2&page_size=1"
"previous": null,
"results": []
}
这个问题对我来说看起来是递归的,这就是我试图做的,并最终通过管道进入递归调用来解决它:
url-encode() {
# A lazy trick to URL-encode strings using `jq`.
printf '"%s"' "$1" | jq --raw-output '@uri'
}
fetch() {
# The first line fed in to `fetch` is the URL we have to fetch
read -r next_url
# The rest of the stdin are the tag names we need to send to stdout
cat
# BASE CASE
#
# A `null` next link means we've just seen the last page, so we can return.
#
if [[ "$next_url" == "null" ]]; then return; fi
# RECURSIVE CASE
#
# 1. Fetch the URL
# 2. Extract the next link and the image tags using `jq`
# 3. Pipe the result into a recursive call
#
echo "Fetching URL: $next_url" >&2
curl --location --silent --show-error --fail "$next_url" \
| jq --raw-output '.next, .results[].name' \
| fetch
}
# We need a way to start off the recursive chain, which we do by sending
# a single line to `fetch` containing the URL of the first page we want
# to fetch.
first() {
local -r repo=$(url-encode "$1")
echo "https://hub.docker.com/v2/repositories/$repo/tags/?page=1&page_size=100"
}
declare -r repo=$1
first "$repo" | fetch
也许这并不理想,我很高兴收到改进它的建议,但出于这个问题的目的,我感兴趣的是是否可以通过使用 FIFO 来解决问题。也许 FIFO 不是完成这项工作的最佳工具,但我最近才发现它们,所以即使它们可能并不理想,我的思想也会尝试应用它们。无论如何,以下是我从 FIFO 角度解决问题时尝试过但失败的方法。
简而言之,我尝试重现问题开头处发布的图表:
first URL → fifo ←───────┐
│ │
↓ ↑
│ │
curl ─────→ tee → stdout
mkfifo urls
# Remove FIFO on script exit.
trap 'rm -f urls' EXIT
fetch() {
local url=$1
# For each line we read from the FIFO, parse it as JSON and extract the
# `next` field. If it's not null, we pass it to `curl` via `xargs`.
#
# The response is both sent to the `urls` FIFO and piped to another `jq`
# call where we keep just what we're interested in — the tag names.
#
cat urls \
| jq --raw-output '.next | select(. != null)' \
| xargs curl --silent \
| tee urls \
| jq --raw-output '.results[].name' &
# The pipeline above is successful in reading the first URL if we take
# out the `tee urls` component of the pipeline. However, the pipeline
# gets stuck if the `tee` component is present.
# Start off the process of fetching by pushing a first URL to the FIFO.
cat <<JSON > urls &
{"next": "$url"}
JSON
# Both previous commands were started off asynchronously (hoping that
# this will achieve the necessary concurrency on the `urls` FIFO), so
# we need to wait on both of them to finish before returning.
wait
}
fetch 'https://hub.docker.com/v2/repositories/library%2Fubuntu/tags/?page=1&page_size=1'
最后,这是我的问题(感谢您阅读到目前为止):
- 为什么上面的方法不起作用?
- 如何更改脚本才能使其正常工作?
谢谢!请告诉我是否应该提供更多详细信息。
答案1
好吧,这里有一个简单的尾调用递归,它基本上减少到一个循环。
next_url=
fetch() {
curl "$1"
# do something with the data
next_url=$( something to produce the next URL or the empty string )
}
next_url=$first_url
# repeat calling `fetch` as long as there is an URL to use
while true; do
fetch "$next_url"
if [[ -z $next_url ]]; then break; fi
done
但是,是的,从循环打印回管道也应该可以。在 Bash 中尝试一下:
mkfifo p
echo 42 > p &
while read x; do
echo $x;
if [[ $x == 0 ]]; then break; fi;
echo $((RANDOM % 5)) >> p;
done < p
它应该打印42
,然后是从 1 到 4 的随机数字,然后是零。
这在任何方面都不是真正的问题,因为您甚至没有同时从多个进程写入管道。
即使您有并发写入器,只要每个单独的行都是使用单个write()
系统调用写入的,这些行就不应该在中间分开。这就是常用工具至少对短字符串所做的事情。 “short-ish”的含义取决于系统,但至少 512 字节的块应该没问题。
使用损坏的工具或较长的字符串,您可能会遇到这样的情况:一行实际上被写成两部分,而另一个编写器有机会插入中间。例如这里写道:
proc #1 proc #2
write("good")
write("hello\n")
write("bye\n")
将向读者显示为两行goodhello
和bye
,而不是按预期显示为goodbye
和。hello
对于管道,您必须对其进行安排,以便读者不需要看到文件结尾。例如,过去read
总是只读一行,或者有更复杂的系统来区分项目的边界。
在您的第二个脚本中,我认为该xargs
调用尝试等到 EOF 后再运行任何内容。由于管道打开供写入tee urls
,因此 EOF 永远不会发生。我真的不会尝试在一个管道中构建它。