Как безопасно и последовательно запускать конвейер?

Использование встроенного расширения подстроки bash:

for f in 2015* ; do
  mv "$f" "${f::4}-${f:4:2}-${f:6}"
done
3
24.08.2017, 00:55
4 ответа

Мы не знаем размер вывода cmd1, но каналы имеют ограниченный размер буфера. Как только этот объем данных будет записан в канал, любая последующая запись будет заблокирована до тех пор, пока кто-нибудь не прочитает канал (вид вашего неудачного решения 3).

Вы должны использовать механизм, гарантирующий отсутствие блокировки. Для очень больших данных используйте временный файл. В противном случае, если вы можете позволить себе хранить данные в памяти (в конце концов, это была идея с каналами), используйте это:

result=$(cmd1) && cmd2 < <(printf '%s' "$result")
unset result

Здесь результат cmd1сохраняется в переменной result. ]. Если cmd1выполнено успешно, выполняется cmd2и передаются данные из result. Наконец, результатне установлен для освобождения связанной памяти.

Примечание: ранее я использовал здесь-строку ( <<< "$result") для передачи cmd2данных, но Стефан Шазелас заметил, что bashсоздаст временный файл, который вам не нужен.

Ответы на вопросы в комментариях:

  • Да, команды можно объединять в цепочки вволю:

    result=$(cmd1) \
    && результат=$(cmd2 < <(printf '%s' "$result")) \
    && результат=$(cmd3 < <(printf '%s' "$result")) \
    ...
    && cmdN < <(printf '%s' "$result")
    неустановленный результат
    
  • Нет, приведенное выше решение не подходит для двоичных данных, потому что:

    1. Подстановка команд $(...)съедает все завершающие символы новой строки.
    2. Не указано поведение для символов NUL ( \0) в результате подстановки команд (например, Bash отбрасывает их).
  • Да, чтобы обойти все эти проблемы с двоичными данными, вы можете использовать кодировщик, например base64(или uuencode, или самодельный, который обрабатывает только символы NUL. и завершающие символы новой строки):

    результат=$(cmd1 > >(base64)) && cmd2 <<(printf '%s' "$result" | base64 -d)
    неустановленный результат
    

    Здесь мне пришлось использовать замену процесса ( >(...)), чтобы сохранить выходное значение cmd1нетронутым.

Тем не менее, опять же, это кажется довольно хлопотным только для того, чтобы гарантировать, что данные не будут записываться на диск. Промежуточный временный файл является лучшим решением. См. ответ Стефана, который отвечает на большинство ваших опасений по этому поводу.

3
27.01.2020, 21:12

Весь смысл передачи команд состоит в том, чтобы запускать их одновременно с одним чтением вывода другого. Если вы хотите запускать их последовательно, и если мы сохраним метафору водопровода, вам нужно передать вывод первой команды в ведро (сохранить его), а затем очистить ведро в другой команде.

Но делать это с помощью каналов означает иметь два процесса для первой команды (команда и другой процесс, считывающий ее вывод с другого конца канала для сохранения в ведре) и два для второго (один из которых очищает ведро). в один конец канала, чтобы команда читала его с другого конца).

Для корзины вам понадобится либо память, либо файловая система. Память плохо масштабируется, и вам нужны каналы. Файловая система имеет гораздо больше смысла. Вот для чего /tmp.Обратите внимание, что диски, скорее всего, никогда не увидят данные, поскольку данные могут не быть сброшены туда намного позже (после того, как вы удалите временный файл), и даже если это так, они, скорее всего, все еще останутся в памяти (кэшируются). А когда это не так, тогда данные были бы слишком большими, чтобы в первую очередь поместиться в памяти.

Обратите внимание, что временные файлы постоянно используются в оболочках. В большинстве оболочек здесь документы и здесь строки реализованы с помощью временных файлов.

В:

cat << EOF
foo
EOF

Большинство оболочек создают временный файл, открывают его для записи и чтения, удаляют его, заполняют foo, а затем запускают catсо своим стандартным вводом. продублировано из фд открытого для чтения. Файл удаляется еще до того, как он заполнится (это дает системе понять, что все, что там написано, не должно пережить потерю питания).

Вы можете сделать то же самое здесь с:

tmp=$(mktemp) && {
  rm -f -- "$tmp" &&
    cmd1 >&3 3>&- 4<&- &&
    cmd2 <&4 4<&- 3>&-
} 3> "$tmp" 4< "$tmp"

Тогда вам не нужно беспокоиться об очистке, так как файл удаляется с самого начала. Нет необходимости в дополнительных процессах для получения данных в корзинах и из них, cmd1и cmd2делают это сами по себе.

Если вы хотите сохранить вывод в памяти, использование оболочки для этого не будет хорошей идеей. Первые оболочки, отличные от zsh, не могут хранить произвольные данные в своих переменных. Вам нужно будет использовать какую-то форму кодирования. И затем, чтобы передать эти данные, вам придется дублировать их в памяти несколько раз, если не записывать их на диск при использовании здесь-документа или здесь-строки.

Вместо этого вы можете использовать perl, например:

 perl -MPOSIX -e '
   sub status() {return WIFEXITED($?) ? WEXITSTATUS($?) : WTERMSIG($?) | 128}
   $/ = undef;
   open A, "-|", "cmd1" or die "open A: $!\n";
   $out = <A>;
   close A;
   $status = status;
   exit $status if $status != 0;

   open B, "|-", "cmd2" or die "open B: $!\n";
   print B $out;
   close B;
   exit status'
2
27.01.2020, 21:12

Во-первых, ваш пример false | echo okне имеет смысла, поскольку falseничего не выводит на свой стандартный вывод, а echoне читает со своего стандартного ввода. «Решение» этого false && echo ok.

cmd1 && cmd2

Это запустит cmd1и не запустит cmd2, пока cmd1не завершит успешное выполнение.

В конвейере, таком как

cmd1 | cmd2

две команды всегда запускаются одновременно (это то, что вы заметили в своем «Неудачное решение 1» ). Их синхронизирует чтение cmd2из вывода cmd1. Конвейер — это способ передачи вывода одной программы во ввод другой, параллельно работающей программы.

Чтобы имитировать, что cmd1выводит что-то, что читает cmd2, но чтобы избавиться от параллелизма, вам нужно сохранить вывод из cmd1во временный файл, который cmd2читает:

cmd1 >outfile && cmd2 <outfile

Временный файл может обрабатываться следующим образом:

trap 'rm -f "$tmpfile"' EXIT
tmpfile=$(mktemp)

cmd1 >"$tmpfile" && cmd2 <"$tmpfile"

Это устанавливает ловушку, которая срабатывает при выходе из оболочки. Ловушка удалит временный файл.

Если у вас есть $TMPDIRв файловой системе памяти, вы не понесете никаких штрафов за ввод-вывод за запись на диск.

Если вас беспокоит размер файла, то вы будете вынуждены хранить его на диске во что бы то ни стало (канал также не сможет удерживать содержимое, это то, что вы заметили в своем " Неудачное решение 3" ).


Глядя на решение xhienne для Bash:

result=$(cmd1) && cmd2 <<< "$result"
unset result

Это работает, если результатом является текст, который не заканчивается пустыми строками, но не работает, если он содержит нулевые байты (они будут отброшеныbash).

Чтобы смягчить это, мы могли бы base64 -закодировать результат:

set -o pipefail # ksh/zsh/bash
result=$( cmd1 | base64 ) && base64 -d <<<"$result" | cmd2
unset result

Это ужасная идея с точки зрения использования памяти и процессора.особенно если результат большой (, кодировка base64 $resultбудет на треть больше, чем двоичная ). Гораздо лучше записать двоичный результат на диск и прочитать его оттуда.

Также обратите внимание, что bashреализует <<<с использованием временного файла в любом случае.

1
27.01.2020, 21:12

Вот откровенно ужасный вариант сшивания разных инструментов изmoreutils:

chronic sh -c '! { echo 123 ; false ; }' | mispipe 'ifne -n false' 'ifne echo ok'

Это все еще не совсем то, что вам нужно :он возвращает 1 в случае неудачи и ноль в противном случае. Однако он не запускает вторую команду до тех пор, пока первая не завершится успешно, он возвращает неудачный или успешный код в зависимости от того, сработала первая команда или нет, и не использует файлы.

Более общая версия:

chronic sh -c '! '"$CMD1" | mispipe 'ifne -n false' "ifne $CMD2"

Это объединяет три инструмента moreutils:

  • chronicспокойно выполняет команду, если она не завершается ошибкой. В этом случае мы запускаем оболочку для запуска вашей первой команды, чтобы мы могли инвертировать результат успеха/неудачи :, он будет запускать команду тихо , если она не удалась, и печатать вывод в конец, если это удается.
  • mispipeсоединяет две команды вместе, возвращая статус выхода первой. Это похоже на эффект set -o pipefail. Команды предоставляются в виде строк, чтобы их можно было отличить друг от друга.
  • ifneзапускает программу, если стандартный ввод не пуст -или пуст с -n. Мы используем его дважды:

    • Первый — ifne -n false. Это запускает falseи использует его в качестве кода выхода, если и только если ввод пуст (, что означает, что chronicсъел его, что означает, что cmd1не удалось ).

      Когда ввод не пуст, он не запускается false, пропускает ввод, как cat, и выходит с 0. Вывод будет передан в следующую команду с помощью mispipe.

    • Второй — ifne cmd2. Это запустит cmd2, если вход не является -пустым . Эти входные данные являются выходными данными ifne -n false, которые будут не -пустыми точно в тот момент, когда выходные данные chronicне были -пустыми,что происходит, когда команда выполнена успешно.

      Когда вход пуст, cmd2никогда не запускается, а ifneвыходит из нуля. mispipeвсе равно отбрасывает выходное значение.


В этом подходе есть (как минимум )два оставшихся недостатка:

  1. Как уже упоминалось, он теряет фактический код выхода cmd1, уменьшая его до логического значения true/false. Если код выхода имеет смысл, он потерян. Можно было бы сохранить код в файл в команде shи повторно -загрузить его позже(ifne -n sh -c 'read code <FILENAME ; rm -f FILENAME; exit $code'или что-то в этом роде ), если это необходимо.
  2. Если cmd1когда-нибудь удастся добиться успеха без результата, все равно все развалится.

Плюс, конечно же, несколько довольно -редких команд, собранных вместе, тщательно процитированных, с неочевидным -значением.

0
27.01.2020, 21:12

Теги

Похожие вопросы