كيف يمكنني إرسال stdout لعملية واحدة إلى عمليات متعددة باستخدام أنابيب (يفضل أن تكون غير مسماة) في Unix (أو Windows)؟
سؤال
أرغب في إعادة توجيه stdout للعملية proc1 إلى عمليتين proc2 وproc3:
proc2 -> stdout
/
proc1
\
proc3 -> stdout
حاولت
proc1 | (proc2 & proc3)
ولكن لا يبدو أن العمل، أي.
echo 123 | (tr 1 a & tr 1 b)
يكتب
b23
إلى stdout بدلا من
a23
b23
المحلول
ملحوظة المحرر:
- >(…)
هو استبدال العملية هذا هو ميزة الصدفة غير القياسية ل بعض الأصداف المتوافقة مع POSIX: bash
, ksh
, zsh
.
- ترسل هذه الإجابة بطريق الخطأ مخرجات استبدال عملية الإخراج عبر خط الأنابيب أيضاً: echo 123 | tee >(tr 1 a) | tr 1 b
.
- سيتم تشذير المخرجات من بدائل العملية بشكل غير متوقع، باستثناء zsh
, ، قد ينتهي خط الأنابيب قبل الأوامر الموجودة بالداخل >(…)
يفعل.
في نظام التشغيل Unix (أو نظام التشغيل Mac)، استخدم ملف tee
يأمر:
$ echo 123 | tee >(tr 1 a) >(tr 1 b) >/dev/null
b23
a23
عادة سوف تستخدم tee
لإعادة توجيه الإخراج إلى ملفات متعددة ، ولكن باستخدام> (...) يمكنك إعادة توجيه عملية أخرى.لذلك، بشكل عام،
$ proc1 | tee >(proc2) ... >(procN-1) >(procN) >/dev/null
سوف تفعل ما تريد.
تحت النوافذ، لا أعتقد أن القشرة المدمجة لها ما يعادلها.مايكروسوفت ويندوز باورشيل لديه tee
الأمر بالرغم من ذلك.
نصائح أخرى
وكما قال dF bash
يسمح باستخدام >(…)
إنشاء تشغيل أمر بدلاً من اسم الملف.(وهناك أيضا <(…)
بناء ليحل محل انتاج أمر آخر بدلاً من اسم الملف، ولكن هذا غير ذي صلة الآن، أذكره فقط للاكتمال).
إذا لم يكن لديك bash، أو كنت تعمل على نظام به إصدار أقدم من bash، فيمكنك القيام بما يفعله bash يدويًا، من خلال الاستفادة من ملفات FIFO.
الطريقة العامة لتحقيق ما تريد هي:
- حدد عدد العمليات التي يجب أن تتلقى مخرجات الأمر الخاص بك، وقم بإنشاء أكبر عدد ممكن من عمليات FIFO، ويفضل أن يكون ذلك في مجلد مؤقت عام:
subprocesses="a b c d" mypid=$$ for i in $subprocesses # this way we are compatible with all sh-derived shells do mkfifo /tmp/pipe.$mypid.$i done
- ابدأ جميع العمليات الفرعية الخاصة بك في انتظار الإدخال من FIFOs:
for i in $subprocesses do tr 1 $i </tmp/pipe.$mypid.$i & # background! done
- قم بتنفيذ الأمر الخاص بك إلى FIFOs:
proc1 | tee $(for i in $subprocesses; do echo /tmp/pipe.$mypid.$i; done)
- أخيرًا، قم بإزالة FIFOs:
for i in $subprocesses; do rm /tmp/pipe.$mypid.$i; done
ملحوظة:لأسباب التوافق، سأفعل $(…)
مع الاقتباسات الخلفية، لكنني لم أتمكن من كتابة هذه الإجابة (يتم استخدام الاقتباس الخلفي في SO).عادة، $(…)
قديم بما يكفي للعمل حتى في الإصدارات القديمة من ksh، ولكن إذا لم يكن كذلك، قم بإرفاق ملف …
جزء في الاقتباسات الخلفية.
يونكس (bash
, ksh
, zsh
)
إجابة dF يحتوي على بذرة من الجواب على أساس tee
و انتاج بدائل العملية
(>(...)
) الذي - التي يمكن اه ويمكن لا العمل حسب متطلباتك:
لاحظ أن بدائل العملية هي أ غير قياسي ميزة (في الغالب) قذائف Posix-Features فقط مثل dash
(الذي يعمل ك /bin/sh
على Ubuntu ، على سبيل المثال) ، افعل لا يدعم.استهداف البرامج النصية شل /bin/sh
يجب لا تعتمد عليها.
echo 123 | tee >(tr 1 a) >(tr 1 b) >/dev/null
ال المزالق من هذا النهج هي:
سلوك الإخراج غير المتزامن وغير المتوقع:تدفقات الإخراج من الأوامر داخل بدائل عملية الإخراج
>(...)
تتداخل بطرق غير متوقعة.في
bash
وksh
(في مقابلzsh
- ولكن انظر الاستثناء أدناه):- قد يصل الإخراج بعد انتهى الأمر.
- قد يبدأ تنفيذ الأوامر اللاحقة قبل انتهت الأوامر الموجودة في عملية الاستبدال -
bash
وksh
يفعل لا انتظر حتى تنتهي العمليات الناتجة عن استبدال عملية الإخراج، على الأقل افتراضيًا. - jmb يضعها جيدًا في تعليق على إجابة dF.:
انتبه إلى أن الأوامر بدأت في الداخل
>(...)
يتم فصلها عن الغلاف الأصلي، ولا يمكنك بسهولة تحديد متى تنتهي؛الtee
ستنتهي بعد كتابة كل شيء، لكن العمليات البديلة ستظل تستهلك البيانات من المخازن المؤقتة المختلفة في النواة وإدخال/إخراج الملف، بالإضافة إلى أي وقت يستغرقه التعامل الداخلي مع البيانات.يمكنك أن تواجه حالات سباق إذا استمرت غلافك الخارجي في الاعتماد على أي شيء تنتجه العمليات الفرعية.
zsh
هي القشرة الوحيدة التي يفعل بشكل افتراضي، انتظر حتى تنتهي العمليات التي يتم تشغيلها في بدائل عملية الإخراج, يستثني اذا كانت com.stderr يتم إعادة توجيهه إلى واحد (2> >(...)
).ksh
(على الأقل اعتبارًا من الإصدار93u+
) يسمح باستخدام وسيطة أقلwait
لانتظار انتهاء العمليات التي تم إنشاؤها بواسطة استبدال عملية الإخراج.
لاحظ أنه في الجلسة التفاعلية قد يؤدي ذلك إلى انتظار أي تعليق وظائف الخلفية أيضا، ولكن.bash v4.4+
يمكن أن تنتظر آخر المستجدات أطلقت استبدال عملية الإخراج معwait $!
, ، ولكن بلا حجةwait
يفعل لا العمل، مما يجعل هذا غير مناسب لأمر به عديد بدائل عملية الإخراجلكن،
bash
وksh
يمكن ان يكون قسري بالانتظار \ في الانتظار عن طريق توجيه الأمر إلى| cat
, ، ولكن لاحظ أن هذا يجعل الأمر يعمل في ملف قذيفة فرعية. تحفظات:ksh
(اعتبارا منksh 93u+
) لا يدعم الإرسال com.stderr لاستبدال عملية الإخراج (2> >(...)
);مثل هذه المحاولة هي تجاهلت بصمت.بينما
zsh
هو (جدير بالثناء) متزامن بشكل افتراضي مع (أكثر شيوعا) com.stdout بدائل عملية الإخراج، حتى| cat
لا يمكن للتقنية أن تجعلها متزامنة مع com.stderr بدائل عملية الإخراج (2> >(...)
).
لكن، حتى لو تأكدت التنفيذ المتزامن, ، مشكلة إخراج معشق بشكل غير متوقع بقايا.
الأمر التالي، عند تشغيله bash
أو ksh
, ، يوضح السلوكيات الإشكالية (قد تضطر إلى تشغيله عدة مرات لترى كلاهما أعراض):ال AFTER
سيتم طباعة عادة قبل الناتج من بدائل المخرجات، ويمكن تشذير الناتج من الأخير بشكل غير متوقع.
printf 'line %s\n' {1..30} | tee >(cat -n) >(cat -n) >/dev/null; echo AFTER
باختصار:
ضمان تسلسل إخراج معين لكل أمر:
- لا
bash
ولاksh
ولاzsh
دعم ذلك.
- لا
التنفيذ المتزامن:
- قابلة للتنفيذ، إلا مع com.stderr-بدائل عملية الإخراج المصدرية:
- في
zsh
, ، هم بثبات غير متزامن. - في
ksh
, ، هم لا تعمل على الإطلاق.
- في
- قابلة للتنفيذ، إلا مع com.stderr-بدائل عملية الإخراج المصدرية:
إذا كنت تستطيع التعايش مع هذه القيود، فإن استخدام بدائل عمليات الإخراج يعد خيارًا قابلاً للتطبيق (على سبيل المثال، إذا كانت جميعها تكتب في ملفات إخراج منفصلة).
لاحظ أن tzot هو الحل الأكثر تعقيدًا، ولكن من المحتمل أن يكون متوافقًا مع POSIX يُظهر أيضًا سلوك إخراج غير متوقع;ومع ذلك، باستخدام wait
يمكنك التأكد من عدم بدء تنفيذ الأوامر اللاحقة إلا بعد انتهاء جميع العمليات في الخلفية.
انظر القاع ل تنفيذ مخرجات أكثر قوة وتزامنًا وتسلسلًا.
الوحيد واضحة bash
حل مع سلوك الإخراج يمكن التنبؤ به هو ما يلي، والذي، مع ذلك، هو بطيئة للغاية مع مجموعات الإدخال الكبيرة, لأن حلقات الصدفة بطيئة بطبيعتها.
لاحظ أيضًا أن هذا المناوبين خطوط الإخراج من الأوامر الهدف.
while IFS= read -r line; do
tr 1 a <<<"$line"
tr 1 b <<<"$line"
done < <(echo '123')
يونكس (باستخدام جنو الموازي)
التثبيت جنو parallel
تمكن أ حل قوي مع الإخراج المتسلسل (لكل أمر). الذي يسمح بالإضافة إلى ذلك التنفيذ الموازي:
$ echo '123' | parallel --pipe --tee {} ::: 'tr 1 a' 'tr 1 b'
a23
b23
parallel
بشكل افتراضي يضمن عدم تداخل الإخراج من الأوامر المختلفة (يمكن تعديل هذا السلوك - راجع man parallel
).
ملحوظة:تأتي بعض توزيعات Linux مع ملحق مختلف parallel
الأداة المساعدة، والتي لن تعمل مع الأمر أعلاه؛يستخدم parallel --version
لتحديد أي واحد لديك، إن وجد.
شبابيك
إجابة جاي بازوزي المفيدة يوضح كيفية القيام بذلك في بوويرشيل.هكذا قال:إجابته هي التناظرية للحلقات bash
الإجابة أعلاه، سيكون بطيئة للغاية مع مجموعات الإدخال الكبيرة و أيضا المناوبين خطوط الإخراج من الأوامر الهدف.
bash
حل Unix قائم على نظام Unix ولكنه محمول مع التنفيذ المتزامن وتسلسل المخرجات
ما يلي هو تنفيذ بسيط، ولكن قوي إلى حد معقول للنهج المعروض في إجابة تسوت الذي يوفر بالإضافة إلى ذلك:
- التنفيذ المتزامن
- الإخراج المتسلسل (المجمع).
على الرغم من أنه ليس متوافقًا بشكل صارم مع POSIX، لأنه bash
البرنامج النصي، ينبغي أن يكون محمول على أي منصة يونكس التي لديها bash
.
ملحوظة:يمكنك العثور على تطبيق أكثر اكتمالاً تم إصداره بموجب ترخيص MIT في هذا جوهر.
إذا قمت بحفظ الكود أدناه كبرنامج نصي fanout
, ، اجعله قابلاً للتنفيذ وأدخله في ملفك PATH
, ، سيعمل الأمر من السؤال على النحو التالي:
$ echo 123 | fanout 'tr 1 a' 'tr 1 b'
# tr 1 a
a23
# tr 1 b
b23
fanout
كود مصدر البرنامج النصي:
#!/usr/bin/env bash
# The commands to pipe to, passed as a single string each.
aCmds=( "$@" )
# Create a temp. directory to hold all FIFOs and captured output.
tmpDir="${TMPDIR:-/tmp}/$kTHIS_NAME-$$-$(date +%s)-$RANDOM"
mkdir "$tmpDir" || exit
# Set up a trap that automatically removes the temp dir. when this script
# exits.
trap 'rm -rf "$tmpDir"' EXIT
# Determine the number padding for the sequential FIFO / output-capture names,
# so that *alphabetic* sorting, as done by *globbing* is equivalent to
# *numerical* sorting.
maxNdx=$(( $# - 1 ))
fmtString="%0${#maxNdx}d"
# Create the FIFO and output-capture filename arrays
aFifos=() aOutFiles=()
for (( i = 0; i <= maxNdx; ++i )); do
printf -v suffix "$fmtString" $i
aFifos[i]="$tmpDir/fifo-$suffix"
aOutFiles[i]="$tmpDir/out-$suffix"
done
# Create the FIFOs.
mkfifo "${aFifos[@]}" || exit
# Start all commands in the background, each reading from a dedicated FIFO.
for (( i = 0; i <= maxNdx; ++i )); do
fifo=${aFifos[i]}
outFile=${aOutFiles[i]}
cmd=${aCmds[i]}
printf '# %s\n' "$cmd" > "$outFile"
eval "$cmd" < "$fifo" >> "$outFile" &
done
# Now tee stdin to all FIFOs.
tee "${aFifos[@]}" >/dev/null || exit
# Wait for all background processes to finish.
wait
# Print all captured stdout output, grouped by target command, in sequences.
cat "${aOutFiles[@]}"
منذ @dF:ذكرت أن PowerShell لديه نقطة الإنطلاق، واعتقدت أنني سأعرض طريقة للقيام بذلك في PowerShell.
PS > "123" | % {
$_.Replace( "1", "a"),
$_.Replace( "2", "b" )
}
a23
1b3
لاحظ أن كل كائن يخرج من الأمر الأول تتم معالجته قبل إنشاء الكائن التالي.يمكن أن يسمح هذا بالتوسيع إلى مدخلات كبيرة جدًا.
طريقة أخرى للقيام بذلك ستكون،
eval `echo '&& echo 123 |'{'tr 1 a','tr 1 b'} | sed -n 's/^&&//gp'`
انتاج:
a23
b23
لا حاجة لإنشاء قذيفة فرعية هنا