命名管道/FIFO 可以与“tee”一起以“循环”方式使用吗?

命名管道/FIFO 可以与“tee”一起以“循环”方式使用吗?

为什么这个脚本的最后一行会卡住?

#!/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. 为什么上面的方法不起作用?
  2. 如何更改脚本才能使其正常工作?

谢谢!请告诉我是否应该提供更多详细信息。

答案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")

将向读者显示为两行goodhellobye,而不是按预期显示为goodbye和。hello


对于管道,您必须对其进行安排,以便读者不需要看到文件结尾。例如,过去read总是只读一行,或者有更复杂的系统来区分项目的边界。

在您的第二个脚本中,我认为该xargs调用尝试等到 EOF 后再运行任何内容。由于管道打开供写入tee urls,因此 EOF 永远不会发生。我真的不会尝试在一个管道中构建它。

相关内容