Мониторинг файла, пока не найдена строка

84515
Alex Hofsteede

Я использую tail -f для мониторинга файла журнала, в который ведется активная запись. Когда в файл журнала записывается определенная строка, я хочу выйти из режима мониторинга и продолжить работу с остальным сценарием.

В настоящее время я использую:

tail -f logfile.log | grep -m 1 "Server Started" 

Когда строка найдена, grep завершает работу, как и ожидалось, но мне нужно найти способ заставить команду tail выйти так, чтобы скрипт мог продолжаться.

47
Интересно, на какой операционной системе работал оригинальный постер? В системе Linux RHEL5 я с удивлением обнаружил, что команда tail просто умирает, когда команда grep нашла совпадение и завершила работу. ZaSter 10 лет назад 0
@ZaSter: «Хвост» умирает только на следующей строке. Попробуйте это: `date> log; бревно | grep -m 1 trigger`, а затем в другой оболочке: `echo trigger >> log`, и вы увидите вывод` trigger` в первой оболочке, но без завершения команды. Затем попробуйте: `date >> log` во второй оболочке, и команда в первой оболочке завершится. Но иногда это слишком поздно; мы хотим завершить работу, как только появится триггерная линия, а не после завершения строки после триггерной линии. Alfe 9 лет назад 3
Это отличное объяснение и пример, @Alfe. ZaSter 9 лет назад 0
элегантное надежное решение в одну строку состоит в том, чтобы [использовать `tail` +` grep -q`, как в ответе 00promeheus] (https://superuser.com/a/900134/28756) Trevor Boyd Smith 7 лет назад 1

18 ответов на вопрос

51
Rob Whelan

Принятый ответ не работает для меня, плюс он сбивает с толку и меняет файл журнала.

Я использую что-то вроде этого:

tail -f logfile.log | while read LOGLINE do [[ "$" == *"Server Started"* ]] && pkill -P $$ tail done 

Если строка журнала соответствует шаблону, уничтожьте tailзапущенный этим сценарием.

Примечание: если вы также хотите просмотреть вывод на экране, либо | tee /dev/ttyпередайте эхо-строку перед тестированием в цикле while.

Это работает, но `pkill` не указан в POSIX и не доступен везде. Richard Hansen 11 лет назад 2
Вам не нужно время цикла. используйте часы с опцией -g, и вы можете избавиться от неприятной команды pkill. l1zard 9 лет назад 2
@ l1zard Можете ли вы сделать это? Как бы вы смотрели хвост файла журнала, пока не появилась конкретная строка? (Менее важно, но мне также любопытно, когда был добавлен watch -g; у меня есть более новый сервер Debian с этой опцией и еще один старый на основе RHEL без него). Rob Whelan 9 лет назад 0
Мне не совсем понятно, зачем здесь нужен хвост. Насколько я понимаю, это правильно, пользователь хочет выполнить определенную команду, когда появляется определенное ключевое слово в файле журнала. Команда, приведенная ниже с использованием watch, выполняет эту задачу. l1zard 9 лет назад 0
Не совсем - это проверка, когда данная строка * добавляется * в файл журнала. Я использую это для проверки, когда Tomcat или JBoss полностью запущены; они пишут «Сервер запущен» (или подобный) каждый раз, когда это происходит. Rob Whelan 9 лет назад 0
`pkill` не указан в POSIX, но он предустановлен в CentOS, Fedora, Ubuntu, Debian, MacOS и, возможно, по многим другим причинам. ndemou 7 лет назад 0
Использование grep сэкономит вам немного ресурсов процессора. И вы все еще можете использовать pkill. laurent 6 лет назад 0
24
00prometheus

A simple POSIX one-liner

Here is a simple one-liner. It doesn't need bash-specific or non-POSIX tricks, or even a named pipe. All you really need is to decouple the termination of tail from grep. That way, once grep ends, the script can continue even if tail hasn't ended yet. So this simple method will get you there:

( tail -f -n0 logfile.log & ) | grep -q "Server Started" 

grep will block until it has found the string, whereupon it will exit. By making tail run from it's own sub-shell, we can place it in the background so it runs independently. Meanwhile, the main shell is free to continue execution of the script as soon as grep exits. tail will linger in its sub-shell until the next line has been written to the logfile, and then exit (possibly even after the main script has terminated). The main point is that the pipeline no longer waits for tail to terminate, so the pipeline exits as soon as grep exits.

Some minor tweaks:

  • The option -n0 to tail makes it start reading from the current last line of logfile, in case the string exists earlier in the logfile.
  • You might want to give tail -F rather than -f. It is not POSIX, but it allows tail to work even if the log is rotated while waiting.
  • Option -q rather than -m1 makes grep quit after the first occurrence, but without printing out the trigger line. Also it is POSIX, which -m1 isn't.
Этот подход навсегда оставит «хвост» на заднем плане. Как бы вы захватили PID `tail` внутри подпокрытой оболочки и выставили его в качестве основной оболочки? Я могу только предложить необязательный обходной путь, убив все присоединенные к сеансу процессы `tail`, используя` pkill -s 0 tail`. Rick van der Zwet 7 лет назад 2
В большинстве случаев это не должно быть проблемой. Причина, по которой вы делаете это в первую очередь, заключается в том, что вы ожидаете, что в файл журнала будет записано больше строк. `tail` прекратит работу, как только попытается записать в сломанную трубу. Канал прервется, как только завершится `grep`, поэтому, как только завершится` grep`, `tail` прекратит работу после того, как файл журнала получит еще одну строку. 00prometheus 7 лет назад 0
когда я использовал это решение, я ** не ** фоновый `tail -f`. Trevor Boyd Smith 7 лет назад 0
@Trevor Бойд Смит, да, это работает в большинстве ситуаций, но проблема OP заключалась в том, что grep не завершится, пока не завершится tail, и tail не завершит работу, пока в файле журнала не появится другая строка _after_ grep (когда tail пытается кормить трубопровод, который был прерван по окончании grep). Поэтому, если вы не справитесь с задним фоном, ваш скрипт не продолжит выполнение до тех пор, пока в файле журнала не появится дополнительная строка, а не точно та строка, которая перехватывает grep. 00prometheus 7 лет назад 2
«След не выйдет, пока не появится другая строка после [требуемого шаблона строки]»: это очень тонко, и я полностью пропустил это. Я не заметил, потому что шаблон, который я искал, был посередине, и все это было распечатано быстро. (Опять же, поведение, которое вы описываете, очень тонкое) Trevor Boyd Smith 7 лет назад 0
Да, это довольно тонкая проблема, и для ее понимания требуется распаковка. Извините, это лучшее, что я мог сделать! :-) 00prometheus 7 лет назад 0
Я снова отредактировал свой ответ, чтобы попытаться объяснить немного более четко, но этот материал немного загадочный, поэтому я не очень хорошо это делаю. 00prometheus 7 лет назад 0
13
Richard Hansen

Есть несколько способов добраться tailдо выхода:

Плохой подход: заставить tailнаписать еще одну строку

Вы можете принудительно tailнаписать еще одну строку вывода сразу после того, grepкак нашли совпадение и вышли. Это приведет tailк тому SIGPIPE, что он получит выход. Один из способов сделать это - изменить файл, отслеживаемый tailпосле grepвыхода.

Вот пример кода:

tail -f logfile.log | grep -m 1 "Server Started" | { cat; echo >>logfile.log; } 

В этом примере catне будет выходить до grepтех пор, пока не закроет свой стандартный вывод, поэтому tailвряд ли сможет писать в канал до того, как сможет grepзакрыть свой стандартный вывод. catиспользуется для распространения стандартного вывода grepнеизмененного.

Этот подход относительно прост, но есть несколько недостатков:

  • Если grepзакрыть stdout перед закрытием stdin, всегда будет условие гонки: grepзакрытие stdout, запуск catдля выхода, запуск echo, запуск tailдля вывода строки. Если эта строка отправлена ​​до того, grepкак grepу нее была возможность закрыть стандартный ввод, tailона не будет получена, пока не будет SIGPIPEзаписана другая строка.
  • Требуется доступ для записи в файл журнала.
  • Вы должны быть в порядке с изменением файла журнала.
  • Вы можете повредить файл журнала, если произойдет запись одновременно с другим процессом (записи могут чередоваться, что приводит к появлению новой строки в середине сообщения журнала).
  • Этот подход специфичен для tail- он не будет работать с другими программами.
  • Этап третьего трубопровода делает его трудно получить доступ к коду возврата второго этапа трубопровода (если вы не используете расширение POSIX, такие как bash«ы PIPESTATUSмассив). В этом случае это не имеет большого значения, потому grepчто всегда будет возвращать 0, но в целом средняя стадия может быть заменена другой командой, код возврата которой вам небезразличен (например, что-то, что возвращает 0 при обнаружении «запуска сервера», 1 когда "сервер не запустился" обнаружено).

Следующие подходы позволяют избежать этих ограничений.

Лучший подход: избегайте трубопроводов

Вы можете использовать FIFO, чтобы полностью избежать конвейера, позволяя продолжить выполнение после grepвозврата. Например:

fifo=/tmp/tmpfifo.$$ mkfifo "$" || exit 1 tail -f logfile.log >$ & tailpid=$! # optional grep -m 1 "Server Started" "$" kill "$" # optional rm "$" 

Строки, отмеченные комментарием, # optionalмогут быть удалены, и программа все равно будет работать; tailбудет просто задерживаться, пока не прочитает другую строку ввода или не будет уничтожен каким-либо другим процессом.

Преимущества этого подхода:

  • вам не нужно изменять файл журнала
  • подход работает для других утилит, кроме tail
  • он не страдает от состояния гонки
  • вы можете легко получить возвращаемое значение grep(или любую другую альтернативную команду, которую вы используете)

Недостатком этого подхода является сложность, особенно управление FIFO: вам нужно будет безопасно сгенерировать временное имя файла, и вам нужно будет убедиться, что временный FIFO удален, даже если пользователь нажимает Ctrl-C в середине сценарий. Это можно сделать с помощью ловушки.

Альтернативный подход: отправить сообщение в Kill tail

Вы можете получить выход из tailэтапа конвейера, отправив ему сигнал типа SIGTERM. Задача состоит в том, чтобы точно знать две вещи в одном и том же месте в коде: tailPID и был ли grepвыход.

При использовании конвейерного типа tail -f ... | grep ...легко изменить первый этап конвейера, чтобы сохранить tailPID в переменной с помощью фонового изображения tailи чтения $!. Также легко изменить второй этап конвейера для запуска killпри grepвыходе. Проблема заключается в том, что два этапа конвейера работают в отдельных «средах выполнения» (в терминологии стандарта POSIX), поэтому второй этап конвейера не может читать переменные, установленные первым этапом конвейера. Без использования переменных оболочки либо второй этап должен каким-то образом определять tailPID, чтобы он мог завершиться tailпри grepвозврате, либо первый этап должен быть каким-то образом уведомлен при grepвозврате.

Второй этап можно использовать pgrepдля получения tailPID, но это будет ненадежно (вы могли бы соответствовать неправильному процессу) и непереносимо ( pgrepне указано в стандарте POSIX).

Первый этап может отправить PID на второй этап через канал, echoиспользуя PID, но эта строка будет смешана с tailвыходными данными. Демультиплексирование двух может потребовать сложной схемы экранирования, в зависимости от вывода tail.

Вы можете использовать FIFO, чтобы второй этап конвейера уведомлял первый этап конвейера при grepвыходе. Тогда первый этап может убить tail. Вот пример кода:

fifo=/tmp/notifyfifo.$$ mkfifo "$" || exit 1 { # run tail in the background so that the shell can # kill tail when notified that grep has exited tail -f logfile.log & # remember tail's PID tailpid=$! # wait for notification that grep has exited read foo <$ # grep has exited, time to go kill "$" } | { grep -m 1 "Server Started" # notify the first pipeline stage that grep is done echo >$ } # clean up rm "$" 

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

Предупреждение о буферизации

POSIX позволяет полностью буферизовать потоки stdin и stdout, что означает, что tailвыходные данные могут не обрабатываться grepсколь угодно долго. В системах GNU не должно быть никаких проблем: GNU grepиспользует read(), что исключает любую буферизацию, а GNU tail -fделает регулярные вызовы fflush()при записи в stdout. В системах без GNU может потребоваться сделать что-то особенное, чтобы отключить или регулярно очищать буферы.

Ваше решение (как и другие, я не буду винить вас) упустит то, что уже записано в файл журнала до начала вашего мониторинга. `Tail -f` выведет только последние десять строк, а затем все последующие. Чтобы улучшить это, вы можете добавить опцию `-n 10000` к хвосту, чтобы также были выделены последние 10000 строк. Alfe 9 лет назад 0
Другая идея: я думаю, что ваше решение fifo можно исправить, пропустив вывод `tail -f` через fifo и щелкнув по нему:` mkfifo f; tail -f log> f & tailpid = $! ; grep -m 1 триггер f; убить $ tailpid; rm f`. Alfe 9 лет назад 0
@Alfe: Я могу ошибаться, но я считаю, что запись `tail -f log` в FIFO приведет к тому, что некоторые системы (например, GNU / Linux) будут использовать блочную буферизацию вместо строчной буферизации, что означает` grep `может не увидеть соответствующую строку, когда она появится в журнале. Система может предоставить утилиту для изменения буферизации, такую ​​как `stdbuf` из GNU coreutils. Однако такая утилита была бы непереносимой. Richard Hansen 9 лет назад 0
@Alfe: На самом деле, похоже, что POSIX ничего не говорит о буферизации, кроме как при взаимодействии с терминалом, поэтому, с точки зрения стандартов, я думаю, что ваше более простое решение так же хорошо, как и мое сложное. Однако я не уверен на 100% о том, как различные реализации ведут себя в каждом конкретном случае. Richard Hansen 9 лет назад 1
На самом деле, теперь я использую еще более простой `grep -q -m 1 триггер <(tail -f log) ', предложенный в другом месте, и согласен с тем фактом, что` tail` работает на одну строку длиннее в фоновом режиме, чем это необходимо. Alfe 9 лет назад 0
@Alfe: Подстановка процесса `<(foo)` специфична для Bash (и Zsh, возможно, других). Он не соответствует POSIX, поэтому он не подходит для использования в переносимом скрипте. Richard Hansen 9 лет назад 0
@Alfe: Кроме того, при использовании `<(tail -f log)` команда `tail -f log` будет продолжать выполняться даже после выхода из` grep`. Это не может быть проблемой в этом случае, но это может быть проблемой в других случаях. Richard Hansen 9 лет назад 0
Как я уже писал: с этим решением `tail` работает на одну строку длиннее необходимого (без остановки завершения` grep` и, следовательно, основной команды), и, по крайней мере, в моих случаях это не проблема. Мне трудно думать о сценарии, в котором это больше, чем академическая проблема. Alfe 9 лет назад 0
** Относительно буферизации и последующих задержек ** быстрое примечание для тех, кто пытается использовать это с любой командой, а не только с `tail`: попробуйте` stdbuf -o0` и `stdbuf -i0` в левой и правой части канала, если Вы испытываете какие-либо задержки. В моем случае я пытался отслеживать `docker-compose logs`, и это был выход, который был буферизован, ничего общего с` grep`. rsilva4 8 лет назад 0
13
petch

Если вы используете Bash (по крайней мере, но кажется, что он не определен POSIX, поэтому он может отсутствовать в некоторых оболочках), вы можете использовать синтаксис

grep -m 1 "Server Started" <(tail -f logfile.log) 

Он работает почти так же, как уже упоминавшиеся решения FIFO, но гораздо проще в написании.

Это работает, но хвост все еще работает, пока вы не отправите `SIGTERM` (Ctrl + C, выйти из команды или убить его) mems 9 лет назад 1
@mems, подойдет любая дополнительная строка в файле журнала. `Tail` прочитает его, попытается вывести его, а затем получит SIGPIPE, который прервет его. Итак, в принципе вы правы; `tail` может работать бесконечно долго, если в файл журнала больше ничего не записывается. На практике это может быть очень аккуратным решением для многих людей. Alfe 9 лет назад 3
8
Elifarley

Позвольте мне расширить ответ @ 00promeheus (который является лучшим).

Может быть, вы должны использовать тайм-аут, а не ждать бесконечно.

Приведенная ниже функция bash будет блокироваться до тех пор, пока не появится заданное условие поиска или не истечет заданное время ожидания.

Статус выхода будет 0, если строка найдена в течение времени ожидания.

wait_str() { local file="$1"; shift local search_term="$1"; shift local wait_time="$"; shift # 5 minutes as default timeout  (timeout $wait_time tail -F -n0 "$file" &) | grep -q "$search_term" && return 0  echo "Timeout of $wait_time reached. Unable to find '$search_term' in '$file'" return 1 } 

Возможно, файл журнала еще не существует только после запуска вашего сервера. В этом случае вам следует подождать, пока он появится, прежде чем искать строку:

wait_server() { echo "Waiting for server..." local server_log="$1"; shift local wait_time="$1"; shift  wait_file "$server_log" 10 || { echo "Server log file missing: '$server_log'"; return 1; }  wait_str "$server_log" "Server Started" "$wait_time" }  wait_file() { local file="$1"; shift local wait_seconds="$"; shift # 10 seconds as default timeout  until test $((wait_seconds--)) -eq 0 -o -f "$file" ; do sleep 1; done  ((++wait_seconds)) } 

Вот как вы можете использовать это:

wait_server "/var/log/server.log" 5m && \ echo -e "\n-------------------------- Server READY --------------------------\n" 
Итак, где же команда `timeout`? ayanamist 7 лет назад 0
На самом деле, использование `timeout` - единственный надежный способ не зависать бесконечно в ожидании сервера, который не может запуститься и уже вышел. gluk47 7 лет назад 0
Этот ответ самый лучший. Просто скопируйте функцию и вызовите ее, это очень просто и многократно Hristo Vrigazov 5 лет назад 1
6
Alex Hofsteede

So after doing some testing, I found a quick 1-line way to make this work. It appears tail -f will quit when grep quits, but there's a catch. It appears to only be triggered if the file is opened and closed. I've accomplished this by appending the empty string to the file when grep finds the match.

tail -f logfile |grep -m 1 "Server Started" | xargs echo "" >> logfile \; 

I'm not sure why the open/close of the file triggers tail to realize that the pipe is closed, so I wouldn't rely on this behavior. but it seems to work for now.

Reason it closes, look at the -F flag, versus the -f flag.

Это работает, потому что добавление в файл журнала приводит к тому, что `tail` выводит другую строку, но к тому времени` grep` завершает свою работу (возможно, там есть условие гонки). Если `grep` вышел к тому времени, когда` tail` пишет другую строку, `tail` получит` SIGPIPE`. Это приводит к немедленному выходу "tail". Richard Hansen 11 лет назад 1
Недостатки этого подхода: (1) есть условие гонки (оно не всегда может завершиться немедленно) (2) требуется доступ для записи в файл журнала (3) вы должны быть в порядке с изменением файла журнала (4) вы можете повредить log-файл (5), он работает только для `tail` (6), вы не можете легко настроить его поведение по-разному в зависимости от разных совпадений строк (« сервер запущен »против« запуск сервера не удался »), потому что вы не можете легко получить код возврата средней ступени конвейера. Существует альтернативный подход, который позволяет избежать всех этих проблем - см. Мой ответ. Richard Hansen 11 лет назад 1
6
mr.spuratic

В настоящее время, как указано, все tail -fрешения, представленные здесь, рискуют подхватить ранее зарегистрированную строку «Server Started» (что может или не может быть проблемой в вашем конкретном случае, в зависимости от количества зарегистрированных строк и ротации файла журнала / усечение).

Вместо того, чтобы слишком усложнять вещи, просто используйте умнее tail, как показал bmike с фрагментом perl. Самое простое решение - это retailвстроенная поддержка регулярных выражений с шаблонами условий запуска и остановки :

retail -f -u "Server Started" server.log > /dev/null 

Это будет следовать за файлом, как обычно, tail -fпока не появится первый новый экземпляр этой строки, а затем завершится. ( -uОпция не срабатывает на существующие строки в последних 10 строках файла в обычном режиме «Follow».)


Если вы используете GNU tail(из coreutils ), следующий простейший вариант - использовать --pidFIFO (именованный канал):

mkfifo $ grep -q -m 1 "Server Started" $ & tail -n 0 -f server.log --pid $! >> $ rm $ 

FIFO используется, потому что процессы должны запускаться отдельно, чтобы получить и передать PID. ФИФО по- прежнему страдает от тех же проблем торчать для своевременной записи, чтобы вызвать tailполучить в SIGPIPE, используйте --pidопцию, так что tailвыходит, когда он замечает, что grepокончилось (обычно используются для контроля писателя процесса, а не на читателе, но tailБезразлично» Т действительно волнует). Опция -n 0используется для tailтого, чтобы старые строки не вызывали совпадения.


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

retail "$" > /dev/null # skip over current content while true; do [ "$" -nt ".$.off" ] &&  retail "$" | grep -q "Server Started" && break sleep 2 done 

* Примечание, то же имя, программа отличается от предыдущей опции.

Вместо того, чтобы зацикливаться на процессоре, сравните временную метку файла с состоянием file ( .$.off) и sleep. Используйте " -T", чтобы указать местоположение файла состояния, если требуется, вышеупомянутый предполагает текущий каталог. Не стесняйтесь пропустить это условие, или в Linux вы можете использовать более эффективное inotifywaitвместо этого:

retail "$" > /dev/null while true; do inotifywait -qq "$" &&  retail "$" | grep -q "Server Started" && break done 
Могу ли я объединить «retail» с тайм-аутом, например: «Если прошло 120 секунд, а retail все еще не прочитал строку, введите код ошибки и выйдите из розницы»? kiltek 6 лет назад 0
@kiltek использует GNU `timeout` (coreutils) для запуска` retail` и просто проверяет код выхода 124 по таймауту (`timeout` убьет любую команду, которую вы используете, чтобы запустить после установленного времени) mr.spuratic 6 лет назад 0
4
bmike

Это будет немного сложнее, так как вам придется войти в управление процессом и сигнализацию. Больше kludgey было бы решением с двумя сценариями, использующим отслеживание PID. Лучше бы использовать именованные каналы, как это.

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

Для быстрого и грязного, одного сценария решения - я бы сделал Perl-скрипт, используя File: Tail

use File::Tail; $file=File::Tail->new(name=>$name, maxinterval=>300, adjustafter=>7); while (defined($line=$file->read)) { last if $line =~ /Server started/; } 

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

Любой из них должен включать в себя лишь небольшое обучение для реализации контроля потока наблюдения, который вы ищете.

используя Bash. мой перл-фу не такой сильный, но я попробую. Alex Hofsteede 13 лет назад 0
Используйте трубы - они любят Баш, а Баш любит их. (и ваше программное обеспечение для резервного копирования будет уважать вас, когда оно попадает в одну из ваших труб) bmike 13 лет назад 0
`maxinterval => 300` означает, что он будет проверять файл каждые пять минут. Поскольку я знаю, что моя строка появится в файле на мгновение, я использую гораздо более агрессивный опрос: `maxinterval => 0.2, Adjusttafter => 10000` Stephen Ostermiller 9 лет назад 0
2
Mykhaylo Adamovych

дождитесь появления файла

while [ ! -f /path/to/the.file ]  do sleep 2; done 

дождитесь появления строки в файле

while ! grep "the line you're searching for" /path/to/the.file  do sleep 10; done 

https://superuser.com/a/743693/129669

Этот опрос имеет два основных недостатка: 1. Он тратит время вычислений, просматривая журнал снова и снова. Рассмотрим `/ path / to / the.file` размером 1,4 ГБ; тогда понятно, что это проблема. 2. После появления записи в журнале он дольше, чем необходимо, в худшем случае - 10 с. Alfe 9 лет назад 2
2
Giancarlo Sportelli

I can't imagine a cleaner solution than this one:

#!/usr/bin/env bash # file : untail.sh # usage: untail.sh logfile.log "Server Started" (echo $BASHPID; tail -f $1) | while read LINE ; do if [ -z $TPID ]; then TPID=$LINE # the first line is used to store the previous subshell PID else echo "$LINE"; [[ "$LINE" == *"${*:2}"* ]] && kill -3 $TPID && break fi done 

ok, maybe the name can be subject to improvements...

Advantages:

  • it doesn't use any special utilities
  • it doesn't write to disk
  • it gracefully quits tail and closes the pipe
  • it is pretty short and easy to understand