blocks-отправка ввода в конвейер подпроцесса python

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

моя система - Linux Ubuntu 9.04 с python 2.6 по умолчанию.

я начал с этим пример документации.

from subprocess import Popen, PIPE
p1 = Popen(["grep", "-v", "not"], stdout=PIPE)
p2 = Popen(["cut", "-c", "1-10"], stdin=p1.stdout, stdout=PIPE)
output = p2.communicate()[0]
print output

это работает, но так как p1 ' s stdin не перенаправляется, я должен ввести материал в терминал для подачи трубы. Когда я набираю ^D закрытие stdin, я получаю результат, который я хочу.

однако я хочу отправить данные в канал, используя строковую переменную python. Сначала я попытался написать на stdin:

p1 = Popen(["grep", "-v", "not"], stdin=PIPE, stdout=PIPE)
p2 = Popen(["cut", "-c", "1-10"], stdin=p1.stdout, stdout=PIPE)
p1.stdin.write('testn')
output = p2.communicate()[0] # blocks forever here

не работает. Я пробовал использовать p2.stdout.read() вместо последней строки, но он также блокирует. Я добавил p1.stdin.flush() и p1.stdin.close() но это тоже не сработало. Я тогда перешел к общению:

p1 = Popen(["grep", "-v", "not"], stdin=PIPE, stdout=PIPE)
p2 = Popen(["cut", "-c", "1-10"], stdin=p1.stdout, stdout=PIPE)
p1.communicate('testn') # blocks forever here
output = p2.communicate()[0] 

так что это еще не все.

я заметил, что запуск одного процесса (например,p1 выше, удалением p2) работает отлично. И передача дескриптора файла p1 (stdin=open(...)) тоже работает. Так что проблема в следующем:

можно ли передавать данные в конвейер из 2 или более подпроцессов в python без блокировки? Почему бы и нет?

я знаю, что могу запустить оболочку и запустить трубопровод в оболочке, но это не то, что я хочу.


обновление 1: следующая подсказка Аарон Digulla ниже Теперь я пытаюсь использовать потоки, чтобы заставить его работать.

сначала я попробовал запустить p1.общайтесь по потоку.

p1 = Popen(["grep", "-v", "not"], stdin=PIPE, stdout=PIPE)
p2 = Popen(["cut", "-c", "1-10"], stdin=p1.stdout, stdout=PIPE)
t = threading.Thread(target=p1.communicate, args=('some datan',))
t.start()
output = p2.communicate()[0] # blocks forever here

ладно, не работал. Пробовал другие комбинации, такие как изменение его на .write() и p2.read(). Ничего. Теперь попробуем противоположный подход:--39-->

def get_output(subp):
    output = subp.communicate()[0] # blocks on thread
    print 'GOT:', output

p1 = Popen(["grep", "-v", "not"], stdin=PIPE, stdout=PIPE)
p2 = Popen(["cut", "-c", "1-10"], stdin=p1.stdout, stdout=PIPE)
t = threading.Thread(target=get_output, args=(p2,)) 
t.start()
p1.communicate('datan') # blocks here.
t.join()

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


обновление 2

Поль Дю Буа ответил ниже с некоторой информацией, поэтому я сделал больше тестов. Я прочитал все subprocess.py модуль и получил, как это работает. Поэтому я попытался применить именно это к коду.

я на linux, но так как я тестировал с потоками, мой первый подход состоял в том, чтобы реплицировать точный потоковый код windows, увиденный на subprocess.py ' s communicate() способ, но для двух процессов вместо один. Вот весь список того, что я пробовал:

import os
from subprocess import Popen, PIPE
import threading

def get_output(fobj, buffer):
    while True:
        chunk = fobj.read() # BLOCKS HERE
        if not chunk:
            break
        buffer.append(chunk)

p1 = Popen(["grep", "-v", "not"], stdin=PIPE, stdout=PIPE)
p2 = Popen(["cut", "-c", "1-10"], stdin=p1.stdout, stdout=PIPE)

b = [] # create a buffer
t = threading.Thread(target=get_output, args=(p2.stdout, b))
t.start() # start reading thread

for x in xrange(100000):
    p1.stdin.write('hello worldn') # write data
    p1.stdin.flush()
p1.stdin.close() # close input...
t.join()

хорошо. Это не сработало. Даже после p1.stdin.close() называлась p2.stdout.read() еще блоки.

затем я попробовал код posix на subprocess.py:

import os
from subprocess import Popen, PIPE
import select

p1 = Popen(["grep", "-v", "not"], stdin=PIPE, stdout=PIPE)
p2 = Popen(["cut", "-c", "1-10"], stdin=p1.stdout, stdout=PIPE)

numwrites = 100000
to_read = [p2.stdout]
to_write = [p1.stdin]
b = [] # create buffer

while to_read or to_write:
    read_now, write_now, xlist = select.select(to_read, to_write, [])
    if read_now:
        data = os.read(p2.stdout.fileno(), 1024)
        if not data:
            p2.stdout.close()
            to_read = []
        else:
            b.append(data)

    if write_now:
        if numwrites > 0:
            numwrites -= 1
            p1.stdin.write('hello world!n'); p1.stdin.flush()
        else:
            p1.stdin.close()
            to_write = []

print b

также блоки select.select(). Распространяя prints вокруг, я нашел это:

  • чтение рабочей. Код читается много раз во время выполнения.
  • написание тоже работает. Данные записываются в p1.stdin.
  • в конце numwrites, p1.stdin.close() называется.
  • , когда select() начинает блокировку, только to_read есть что-то, p2.stdout. to_write уже пуст.
  • os.read() вызов всегда возвращает что-то, так что p2.stdout.close() никогда не вызывается.

вывод из обоих тестов: закрытие stdin первого процесса на конвейере (grep в Примере) не делает его дамп его буферизованного вывода на следующий и умереть.

нет способа заставить его работать?

PS: Я не хочу использовать временный файл, я уже тестировал с файлами, и я знаю, что он работает. И я не хочу использовать Windows.

11 ответов


я узнал, как это сделать.

речь идет не о потоках, а не о Select().

когда я запускаю первый процесс (grep), Он создает два дескриптора файлов низкого уровня, по одному для каждого канала. Давайте назовем их a и b.

когда я запускаю второй процесс, b передается в cut sdtin. Но есть мозг-мертвый дефолт на Popen - close_fds=False.

эффект от этого таков cut также наследует a. Так что grep не могу умереть, даже если я закрыть a, потому что stdin все еще открыт на С (cut игнорирует его).

следующий код теперь работает отлично.

from subprocess import Popen, PIPE

p1 = Popen(["grep", "-v", "not"], stdin=PIPE, stdout=PIPE)
p2 = Popen(["cut", "-c", "1-10"], stdin=p1.stdout, stdout=PIPE, close_fds=True)
p1.stdin.write('Hello World\n')
p1.stdin.close()
result = p2.stdout.read() 
assert result == "Hello Worl\n"

close_fds=True ПО УМОЛЧАНИЮ в системах unix. На windows он закрывается все fds, поэтому оно предотвращает тубопровод.

EDIT:

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

EDIT2: найдено другое решение, с большей помощью pooryorick-вместо использования close_fds=True и вблизи все fds, можно закрыть fds, который принадлежит первому процессу, при выполнении второго, и он будет работать. Закрытие должно быть сделано в ребенке, чтобы preexec_fn функция от Popen очень удобна для этого. При выполнении p2 вы можете сделать:

p2 = Popen(cmd2, stdin=p1.stdout, stdout=PIPE, stderr=devnull, preexec_fn=p1.stdin.close)

работа с большими файлами

два принципа должны применяться единообразно при работе с большими файлами в Python.

  1. поскольку любая процедура ввода-вывода может блокировать, мы должны держите каждый этап трубопровода в другом потоке или процесс. В этом примере мы используем потоки, но подпроцессы позволят вам избежать GIL.
  2. мы должны использовать добавочные читает и пишет так, что мы не ждем EOF перед начинаю прогрессировать.

альтернативой является использование неблокирующего ввода-вывода, хотя это громоздко в стандартный Python. См.gevent для облегченной библиотеки потоков, реализующей синхронный API ввода-вывода с использованием неблокирующих примитивов.

пример кода

мы построим глупый трубопровод, который примерно

{cat /usr/share/dict/words} | grep -v not              \
    | {upcase, filtered tee to stderr} | cut -c 1-10   \
    | {translate 'E' to '3'} | grep K | grep Z | {downcase}

где каждый этап в скобках {} реализуется в Python, в то время как другие используют стандартные внешнее приложение. TL; DR: посмотреть в этом суть.

мы начинаем с ожидаемого импорта.

#!/usr/bin/env python
from subprocess import Popen, PIPE
import sys, threading

Python этапы конвейера

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

def writer(output):
    for line in open('/usr/share/dict/words'):
        output.write(line)
    output.close()
def filter(input, output):
    for line in input:
        if 'k' in line and 'z' in line: # Selective 'tee'
            sys.stderr.write('### ' + line)
        output.write(line.upper())
    output.close()
def leeter(input, output):
    for line in input:
        output.write(line.replace('E', '3'))
    output.close()

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

def spawn(func, **kwargs):
    t = threading.Thread(target=func, kwargs=kwargs)
    t.start()
    return t

создать трубопровода

создание внешних этапов с помощью Popen и этапы Python с использованием spawn. Аргумент bufsize=-1 говорит использовать буферизацию системы по умолчанию (обычно 4 КБ). Это обычно быстрее, чем буферизация по умолчанию (без буферизации) или линии, но вам понадобится буферизация линии, если вы хотите визуально контролировать вывод без ЖАГ.

grepv   = Popen(['grep','-v','not'], stdin=PIPE, stdout=PIPE, bufsize=-1)
cut     = Popen(['cut','-c','1-10'], stdin=PIPE, stdout=PIPE, bufsize=-1)
grepk = Popen(['grep', 'K'], stdin=PIPE, stdout=PIPE, bufsize=-1)
grepz = Popen(['grep', 'Z'], stdin=grepk.stdout, stdout=PIPE, bufsize=-1)

twriter = spawn(writer, output=grepv.stdin)
tfilter = spawn(filter, input=grepv.stdout, output=cut.stdin)
tleeter = spawn(leeter, input=cut.stdout, output=grepk.stdin)

привод конвейера

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

for line in grepz.stdout:
    sys.stdout.write(line.lower())

потоки и процессы очищаются, как только они достигают EOF. Мы можем явно очистить используя

for t in [twriter, tfilter, tleeter]: t.join()
for p in [grepv, cut, grepk, grepz]: p.wait()

в Python-2.6 и ранее

внутри subprocess.Popen звонки fork, настраивает дескрипторы файлов канала и вызывает exec. Дочерний процесс от fork имеет копии всех файловых дескрипторов в Родительском процессе и и копии должны быть закрыты, прежде чем соответствующий читатель получит EOF. Это может быть исправлено путем ручного закрытия труб (либо close_fds=True или до subprocess.Popen) или установка FD_CLOEXEC флаг у exec автоматическое закрытие дескриптора файла. Этот флаг устанавливается автоматически в Python-2.7 и более поздних версиях, см. issue12786. Мы можем получить поведение Python-2.7 в более ранних версиях Python, вызвав

p._set_cloexec_flags(p.stdin)

прежде чем перейти p.stdin в качестве аргумента в последующем subprocess.Popen.


есть три основных трюка, чтобы сделать трубы работать, как ожидалось

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

  2. явно закройте неиспользуемый конец трубы в каждом процессе

  3. разберитесь с буферизацией, отключив ее (опция Python-u), используя pty, или просто заполнение буфера чем-то, что не будет повлиять на данные, (возможно, '\n', но все, что подходит).

примеры в модуле Python " pipeline "(я автор) соответствуют вашему сценарию именно, и сделать шаги низкого уровня достаточно ясными.

http://pypi.python.org/pypi/pipeline/

совсем недавно я использовал модуль подпроцесса как часть производитель-процессор-потребитель-контроллер выкройка:

http://www.darkarchive.org/w/Pub/PythonInteract

этот пример имеет дело с буферизованным stdin, не прибегая к использованию pty, и также иллюстрирует, какие концы труб должны быть закрыты где. Я предпочитаю процессы нить, но принцип тот же. Кроме того, он иллюстрирует синхронизация очередей, в которые подается продукция производителя и собирается продукция потребителя, и как закрыть их чисто (следите за часовыми, вставленными в очереди.) Этот шаблон позволяет создавать новые входные данные на основе последних выходных данных, с учетом рекурсивного обнаружения и обработки.


предлагаемое решение Nosklo быстро сломается, если на приемный конец трубы будет записано слишком много данных:


from subprocess import Popen, PIPE

p1 = Popen(["grep", "-v", "not"], stdin=PIPE, stdout=PIPE)
p2 = Popen(["cut", "-c", "1-10"], stdin=p1.stdout, stdout=PIPE, close_fds=True)
p1.stdin.write('Hello World\n' * 20000)
p1.stdin.close()
result = p2.stdout.read() 
assert result == "Hello Worl\n"

Если этот скрипт не висит на вашем компьютере, просто увеличьте "20000" до того, что превышает размер буферов вашей операционной системы.

это потому, что операционная система буферизует вход в "grep", но как только этот буфер заполнен,p1.stdin.write вызов будет блокировать, пока что-то не прочитает из p2.stdout. В сценариях игрушек вы можете получите способ записи / чтения из канала в том же процессе, но в обычном использовании необходимо писать из одного потока/процесса и читать из отдельного потока/процесса. Это верно для подпроцесса.к popen, ОС.трубы, ОС.popen*, etc.

другой поворот заключается в том, что иногда вы хотите продолжать подавать трубу с элементами, созданными из более раннего выхода той же трубы. Решение состоит в том, чтобы сделать как устройство подачи труб, так и устройство чтения труб асинхронными для программы man и реализовать два очереди: одна между основной программой и устройством подачи труб и одна между основной программой и устройством чтения труб. PythonInteract является примером этого.

подпроцесс-хорошая удобная модель, но потому, что она скрывает детали ОС.к popen и ОС.fork calls это делает под капотом, иногда это может быть сложнее, чем вызовы более низкого уровня, которые он использует. По этой причине подпроцесс не является хорошим способом узнать о том, как межпроцессные каналы действительно работа.


вы должны сделать это в нескольких потоках. В противном случае вы окажетесь в ситуации, когда вы не можете отправлять данные: дочерний p1 не будет читать ваш вход, так как p2 не читает вывод p1, потому что вы не читаете вывод p2.

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

кроме того, вы можете отправить данные в p1 с фоновым потоком и прочитать вывод из p2 в основном потоке. Но с обеих сторон должна быть нить.


отвечая на утверждение носкло (см. другие комментарии к этому вопросу), что это невозможно сделать без close_fds=True:

close_fds=True необходима только если вы оставили другой файл дескрипторы открыты. При открытии нескольких дочерних процессов всегда полезно следите за открытыми файлами, которые могут быть унаследованы, и явно закрывать любые это не нужно:

from subprocess import Popen, PIPE

p1 = Popen(["grep", "-v", "not"], stdin=PIPE, stdout=PIPE)
p1.stdin.write('Hello World\n')
p1.stdin.close()
p2 = Popen(["cut", "-c", "1-10"], stdin=p1.stdout, stdout=PIPE)
result = p2.stdout.read() 
assert result == "Hello Worl\n"

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

но реальная проблема заключается в том, что буферы труб укусят вас за все, кроме примеров игрушек. Как я уже говорил в других моих ответов на этот вопрос, правило не открывайте ваш читатель и ваш писатель в одном процессе/потоке. Кто-нибудь кто хочет использовать подпроцесс модуль для двусторонней связи будет хорошо служил для изучения ОС.труба и ОС.вилка, первый. На самом деле это не так. трудно использовать, если у вас есть хороший пример посмотреть.


Я думаю, что вы можете изучать чужие проблемы. Конечно, как говорит Аарон, если вы пытаетесь быть как производителем до начала трубопровода, так и потребителем конца трубопровода, легко попасть в тупиковую ситуацию. Это проблема, которую решает communicate ().

communicate () не совсем корректен для вас, так как stdin и stdout находятся на разных объектах подпроцесса; но если вы посмотрите на реализацию в subprocess.py вы увидите, что это делает именно то, что предложил Аарон.

как только вы увидите, что communicate читает и пишет, Вы увидите, что во второй попытке communicate () конкурирует с p2 за выход p1:

p1 = Popen(["grep", "-v", "not"], stdin=PIPE, stdout=PIPE)
p2 = Popen(["cut", "-c", "1-10"], stdin=p1.stdout, stdout=PIPE)
# ...
p1.communicate('data\n')       # reads from p1.stdout, as does p2

Я работаю на win32, который определенно имеет разные характеристики ввода-вывода и буферизации, но это работает для меня:

p1 = Popen(["grep", "-v", "not"], stdin=PIPE, stdout=PIPE)
p2 = Popen(["cut", "-c", "1-10"], stdin=p1.stdout, stdout=PIPE)
t = threading.Thread(target=get_output, args=(p2,)) 
t.start()
p1.stdin.write('hello world\n' * 100000)
p1.stdin.close()
t.join()

Я настроил размер ввода для создания тупика при использовании наивного непотопляемого p2.read ()

вы также можете попробовать буферизацию в файл, например!--4-->

fd, _ = tempfile.mkstemp()
os.write(fd, 'hello world\r\n' * 100000)
os.lseek(fd, 0, os.SEEK_SET)
p1 = Popen(["grep", "-v", "not"], stdin=fd, stdout=PIPE)
p2 = Popen(["cut", "-c", "1-10"], stdin=p1.stdout, stdout=PIPE)
print p2.stdout.read()

Это также работает для меня без тупиков.


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

from subprocess import Popen, PIPE
import select

p1 = Popen(["grep", "-v", "not"], stdin=PIPE, stdout=PIPE)
p2 = Popen(["cut", "-c", "1-10"], stdin=p1.stdout, stdout=PIPE, close_fds=True)

data_to_write = 100000 * 'hello world\n'
to_read = [p2.stdout]
to_write = [p1.stdin]
b = [] # create buffer
written = 0


while to_read or to_write:
    read_now, write_now, xlist = select.select(to_read, to_write, [])
    if read_now:
        data = p2.stdout.read(1024)
        if not data:
            p2.stdout.close()
            to_read = []
        else:
            b.append(data)

    if write_now:
        if written < len(data_to_write):
            part = data_to_write[written:written+1024]
            written += len(part)
            p1.stdin.write(part); p1.stdin.flush()
        else:
            p1.stdin.close()
            to_write = []

print b

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

большая проблема в том, что этот код скрипта только работает последовательно с правом сочетание ввода данных и внешних программ. grep и cut оба работают с линии, и поэтому их внутренние буферы ведут себя немного иначе. Если мы используем более универсальная команда, такая как" cat", и записывает меньшие биты данных в канал, фатальное состояние гонки будет всплывать чаще:

from subprocess import Popen, PIPE
import select
import time

p1 = Popen(["cat"], stdin=PIPE, stdout=PIPE)
p2 = Popen(["cat"], stdin=p1.stdout, stdout=PIPE, close_fds=True)

data_to_write = 'hello world\n'
to_read = [p2.stdout]
to_write = [p1.stdin]
b = [] # create buffer
written = 0


while to_read or to_write:
    time.sleep(1)
    read_now, write_now, xlist = select.select(to_read, to_write, [])
    if read_now:
        print 'I am reading now!'
        data = p2.stdout.read(1024)
        if not data:
            p1.stdout.close()
            to_read = []
        else:
            b.append(data)

    if write_now:
        print 'I am writing now!'
        if written < len(data_to_write):
            part = data_to_write[written:written+1024]
            written += len(part)
            p1.stdin.write(part); p1.stdin.flush()
        else:
            print 'closing file'
            p1.stdin.close()
            to_write = []

print b

в этом случае проявятся два разных результата:

write, write, close file, read -> success
write, read -> hang

Итак, я снова бросаю вызов nosklo на любой почтовый код, показывающий использование select.select для обработки произвольного ввода и буферизации труб из a один поток, или upvote мои ответы.

итог: не пытайтесь манипулировать обоими концами трубы из одного потока. Это того не стоит. Видеть трубопровод для хорошего низкого уровня пример того, как это сделать правильно.


Что об использовании SpooledTemporaryFile ? Это обходит (но, возможно, не решает) проблему:

http://docs.python.org/library/tempfile.html#tempfile.SpooledTemporaryFile

вы можете написать его как файл, но на самом деле это блок памяти.

или я совершенно не понимаю...


вот пример использования Popen вместе с ОС.вилка, чтобы выполнить то же самое вещь. Вместо использования close_fds Он просто закрывает трубы в нужное место. Гораздо проще, чем пытаться использовать select.select, и принимает полное преимущество буферов трубы системы.

from subprocess import Popen, PIPE
import os
import sys

p1 = Popen(["cat"], stdin=PIPE, stdout=PIPE)

pid = os.fork()

if pid: #parent
    p1.stdin.close()
    p2 = Popen(["cat"], stdin=p1.stdout, stdout=PIPE)
    data = p2.stdout.read()
    sys.stdout.write(data)
    p2.stdout.close()

else: #child
    data_to_write = 'hello world\n' * 100000
    p1.stdin.write(data_to_write)
    p1.stdin.close()

это гораздо проще, чем вы думаете!

import sys
from subprocess import Popen, PIPE

# Pipe the command here. It will read from stdin.
#   So cat a file, to stdin, like (cat myfile | ./this.py),
#     or type on terminal and hit control+d when done, etc
#   No need to handle this yourself, that's why we have shell's!
p = Popen("grep -v not | cut -c 1-10", shell=True, stdout=PIPE)

nextData = None
while True:
    nextData = p.stdout.read()
    if nextData in (b'', ''):
        break
    sys.stdout.write ( nextData.decode('utf-8') )


p.wait()

этот код написан для Python 3.6, и работает с Python 2.7.

использовать его как:

cat README.md  | python ./example.py

или

python example.py < README.md

для передачи содержимого " README.md " к этой программе.

но.. на данный момент, почему бы просто не использовать "cat" напрямую и не трубить выход, как вы хотите? например:

cat filename | grep -v not | cut -c 1-10

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

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