На користь читача цей рецепт тут
- може бути використаний повторно як oneliner для лову stderr у змінну
- як і раніше надає доступ до коду повернення команди
- Пожертвує тимчасовим дескриптором файлів 3 (який ви, звичайно, можете змінити)
- І не виставляє цей тимчасовий дескриптор файлу внутрішній команді
Якщо ви хочете , щоб зловити stderr
деяких command
в var
ви можете зробити
{ var="$( { command; } 2>&1 1>&3 3>&- )"; } 3>&1;
Згодом у вас все є:
echo "command gives $? and stderr '$var'";
Якщо command
це просто (не щось на кшталт a | b
), ви можете залишити внутрішнє {}
:
{ var="$(command 2>&1 1>&3 3>&-)"; } 3>&1;
Згорнутий у просту функцію для багаторазового використання bash
(ймовірно, потрібна версія 3 і вище для local -n
):
: catch-stderr var cmd [args..]
catch-stderr() { local -n v="$1"; shift && { v="$("$@" 2>&1 1>&3 3>&-)"; } 3>&1; }
Пояснили:
local -n
псевдоніми "$ 1" (що є змінною для catch-stderr
)
3>&1
використовує дескриптор файлів 3 для збереження там точок stdout
{ command; }
(або "$ @") потім виконує команду в заході виводу $(..)
- Зверніть увагу, що тут важливий точний порядок (неправильний спосіб переміщення дескрипторів файлів неправильно):
2>&1
перенаправляє stderr
на захоплення виходу$(..)
1>&3
переспрямовує stdout
від виходу, що захоплює $(..)
назад, до "зовнішнього", stdout
який був збережений у дескрипторі файлів 3. Зауважте, що stderr
все ще посилається на те, де FD 1 вказував раніше: На вихід захоплення$(..)
3>&-
потім закриває дескриптор файлу 3, оскільки він більше не потрібен, таким чином, щоб command
раптом не з’явився якийсь невідомий дескриптор відкритого файлу. Зауважте, що зовнішня оболонка все ще має FD 3 відкритою, але command
її не побачить.
- Останнє є важливим, оскільки деякі програми на зразок
lvm
скаржаться на несподівані дескриптори файлів. І lvm
скаржиться stderr
- просто те, що ми збираємось захопити!
Ви можете зловити будь-який інший дескриптор файлу за цим рецептом, якщо ви адаптуєтесь відповідно. За винятком дескриптора файлів 1, звичайно (тут логіка перенаправлення була б неправильною, але для дескриптора файлів 1 ви можете просто використовувати var=$(command)
як завжди).
Зауважте, що це жертвує дескриптором файлів 3. Якщо вам потрібен цей дескриптор файлу, не соромтеся змінити число. Але майте на увазі, що деякі снаряди (починаючи з 1980-х) могли зрозуміти99>&1
аргументи, 9
за якими слід 9>&1
(це не проблема bash
).
Також зауважте, що зробити FD 3, що може бути налаштований за допомогою змінної, не особливо легко. Це робить речі дуже нечитабельними:
: catch-var-from-fd-by-fd variable fd-to-catch fd-to-sacrifice command [args..]
catch-var-from-fd-by-fd()
{
local -n v="$1";
local fd1="$2" fd2="$3";
shift 3 || return;
eval exec "$fd2>&1";
v="$(eval '"$@"' "$fd1>&1" "1>&$fd2" "$fd2>&-")";
eval exec "$fd2>&-";
}
Примітка безпеки: перші 3 аргументи catch-var-from-fd-by-fd
не слід брати від третьої сторони. Завжди надайте їх явно "статичним" способом.
Тож ні-ні-ні catch-var-from-fd-by-fd $var $fda $fdb $command
, ніколи цього не робіть!
Якщо вам трапиться ім'я змінної змінної, принаймні виконайте наступне:
local -n var="$var"; catch-var-from-fd-by-fd var 3 5 $command
Це все одно не захистить вас від кожного подвигу, але, принаймні, допоможе виявити та уникнути поширених помилок сценарію.
Примітки:
catch-var-from-fd-by-fd var 2 3 cmd..
те саме, що catch-stderr var cmd..
shift || return
це лише певний спосіб запобігти некрасиві помилки, якщо ви забудете дати правильну кількість аргументів. Можливо, закінчення оболонки було б іншим способом (але це ускладнює тестування з командного рядка).
- Порядок був написаний таким, що його легше зрозуміти. Можна переписати функцію так, що вона не потрібна
exec
, але тоді вона стає справді некрасивою.
- Цю процедуру можна переписати на
bash
те, що не потребує такої потребиlocal -n
. Однак тоді ви не можете використовувати локальні змінні, і це стає надзвичайно негарно!
- Також зауважте, що
eval
s використовуються безпечно. Зазвичай eval
вважається небезпечним. Однак у цьому випадку це не більше зла, ніж використання "$@"
(для виконання довільних команд). Однак будь ласка, використовуйте точне та правильне цитування, як показано тут (інакше це стає дуже небезпечно ).
ERROR=$(./useless.sh | sed 's/Output/Useless/' 2>&1 1>/dev/ttyX)