이 스크립트가 마지막 줄에서 멈추는 이유는 무엇입니까?
#!/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
이건 내가 해결해야 할 문제야. Docker Hub API를 사용하여 Docker 이미지의 모든 태그를 가져오는 Bash 유틸리티를 작성하고 싶습니다. 기본 요구 사항은 다음과 같습니다.
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를 발견했기 때문에 이상적이지 않더라도 적용하려고 노력하고 있습니다. 어쨌든, 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 사이의 임의의 숫자, 0을 인쇄해야 합니다 .
동시에 여러 프로세스에서 파이프에 쓰는 것이 아니기 때문에 이것은 어떤 식으로든 실제로 문제가 되지 않습니다.
동시 작성자가 있더라도 각 개별 행이 write()
단일 시스템 호출을 사용하여 작성되는 한 중간에 행이 분할되어서는 안 됩니다. 이것이 최소한 짧은 문자열에 대해 일반적인 도구가 수행하는 작업입니다. "짧다"는 의미는 시스템에 따라 다르지만 최소 512바이트의 청크는 괜찮습니다.
깨진 도구나 긴 문자열을 사용하면 한 줄이 실제로 두 부분으로 작성되고 다른 작성자가 중간에 삽입할 기회가 있는 상황이 발생할 수 있습니다. 예를 들어, 여기에 기록되어 있습니다:
proc #1 proc #2
write("good")
write("hello\n")
write("bye\n")
예상한 합계가 아닌 두 줄의 goodhello
합계 로 독자에게 표시됩니다 .bye
goodbye
hello
파이프의 경우 독자가 파일의 끝을 볼 필요가 없도록 배열해야 합니다. 예를 들어, read
예전에는 한 줄만 읽었거나 프로젝트 경계를 구별하기 위한 더 복잡한 시스템이 있었습니다.
두 번째 스크립트에서는 xargs
호출이 아무것도 실행하기 전에 EOF까지 기다리려고 한다고 생각합니다. 쓰기를 위해 파이프가 열려 있으므로 tee urls
EOF가 발생하지 않습니다. 나는 실제로 그것을 파이프라인에 구축하려고 시도하지 않을 것입니다.