В коде «{exec> / dev / null; }> / dev / null ”что происходит под капотом?

15

Когда вы перенаправляете список команд, который содержит перенаправление exec, exec> / dev / null, похоже, по-прежнему не применяется впоследствии, например, с помощью:

{ exec >/dev/null; } >/dev/null; echo "Hi"

«Привет» печатается.

У меня сложилось впечатление, что {}список команд не считается подоболочкой, если только он не является частью конвейера, поэтому, на exec >/dev/nullмой взгляд, его все равно следует применять в текущей среде оболочки.

Теперь, если вы измените его на:

{ exec >/dev/null; } 2>/dev/null; echo "Hi"

нет ожидаемого выхода; дескриптор файла 1 остается указанным на / dev / null и для будущих команд. Это показано повторным запуском:

{ exec >/dev/null; } >/dev/null; echo "Hi"

который не даст никакого выхода.

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

Что происходит в каждом сценарии с дескриптором файла STDOUT?

РЕДАКТИРОВАТЬ: добавив мой вывод strace:

read(255, "#!/usr/bin/env bash\n{ exec 1>/de"..., 65) = 65
open("/dev/null", O_WRONLY|O_CREAT|O_TRUNC, 0666) = 3
fcntl(1, F_GETFD)                       = 0
fcntl(1, F_DUPFD, 10)                   = 10
fcntl(1, F_GETFD)                       = 0
fcntl(10, F_SETFD, FD_CLOEXEC)          = 0
dup2(3, 1)                              = 1
close(3)                                = 0
close(10)                               = 0
open("/dev/null", O_WRONLY|O_CREAT|O_TRUNC, 0666) = 3
fcntl(1, F_GETFD)                       = 0
fcntl(1, F_DUPFD, 10)                   = 10
fcntl(1, F_GETFD)                       = 0
fcntl(10, F_SETFD, FD_CLOEXEC)          = 0
dup2(3, 1)                              = 1
close(3)                                = 0
dup2(10, 1)                             = 1
fcntl(10, F_GETFD)                      = 0x1 (flags FD_CLOEXEC)
close(10)                               = 0
fstat(1, {st_mode=S_IFCHR|0666, st_rdev=makedev(1, 3), ...}) = 0
ioctl(1, TCGETS, 0x7ffee027ef90)        = -1 ENOTTY (Inappropriate ioctl for device)
write(1, "hi\n", 3)                     = 3
Джои Пабалинас
источник
Это странно; Я не могу воспроизвести close(10). Можете ли вы также опубликовать все содержимое скрипта, на котором вы работали?
ДепрессияДаниэль
@DepressedDaniel Вот полный сценарий и strace: сценарий strace
Joey Pabalinas
У вас есть заблудившийся ;после }, который меняет значение, > /dev/nullчтобы не применять к составному списку в {}конце концов.
ДепрессияДаниэль
@DepressedDaniel Ах, вы совершенно правы! Теперь результат - то, что я ожидаю; Спасибо за ваши ответы!
Джои Пабалинас

Ответы:

17

Давайте следовать

{ exec >/dev/null; } >/dev/null; echo "Hi"

шаг за шагом.

  1. Есть две команды:

    а. { exec >/dev/null; } >/dev/null, с последующим

    б. echo "Hi"

    Оболочка выполняет сначала команду (а), а затем команду (б).

  2. Исполнение { exec >/dev/null; } >/dev/nullвыручки происходит следующим образом:

    а. Во-первых, оболочка выполняет перенаправление >/dev/null и запоминает отмену, когда команда заканчивается .

    б. Затем оболочка выполняется { exec >/dev/null; }.

    с. Наконец, оболочка переключает стандартный вывод обратно туда, где был. (Это тот же механизм, что и в ls -lR /usr/share/fonts >~/FontList.txtперенаправлениях, выполняются только на время выполнения команды, к которой они принадлежат.)

  3. Как только первая команда выполнена, оболочка выполняется echo "Hi". Стандартный вывод там, где он был до первой команды.

AlexP
источник
Есть ли какая-то причина, почему 2a выполняется перед 2b? (справа налево)
Джои Пабалинас
5
Перенаправления должны выполняться перед командой, к которой они применяются, нет? Как они могли работать иначе?
AlexP
Ага, никогда не думал об этом таким образом! Первые два - хорошие ответы; дать немного, прежде чем я выберу один, но я ценю оба объяснения!
Джои Пабалинас
К сожалению, я могу выбрать только один ответ, поэтому я согласен с этим, поскольку он немного менее техничен, и поэтому я думаю, что он сможет помочь даже менее опытным пользователям. Однако @DepressedDaniel была столь же большой ответ здесь , который предлагает более подробно объяснение.
Джои Пабалинас
14

Чтобы не использовать вложенную оболочку или подпроцесс, когда вывод составного списка {}передается по конвейеру >, оболочка сохраняет дескриптор STDOUT перед запуском составного списка и восстанавливает его после. Таким образом, exec >в составном списке не действует после точки, где старый дескриптор восстанавливается как STDOUT.

Давайте посмотрим на соответствующую часть strace bash -c '{ exec >/dev/null; } >/dev/null; echo hi' 2>&1 | cat -n:

   132  open("/dev/null", O_WRONLY|O_CREAT|O_TRUNC, 0666) = 3
   133  fcntl(1, F_GETFD)                       = 0
   134  fcntl(1, F_DUPFD, 10)                   = 10
   135  fcntl(1, F_GETFD)                       = 0
   136  fcntl(10, F_SETFD, FD_CLOEXEC)          = 0
   137  dup2(3, 1)                              = 1
   138  close(3)                                = 0
   139  open("/dev/null", O_WRONLY|O_CREAT|O_TRUNC, 0666) = 3
   140  fcntl(1, F_GETFD)                       = 0
   141  fcntl(1, F_DUPFD, 10)                   = 11
   142  fcntl(1, F_GETFD)                       = 0
   143  fcntl(11, F_SETFD, FD_CLOEXEC)          = 0
   144  dup2(3, 1)                              = 1
   145  close(3)                                = 0
   146  close(11)                               = 0
   147  dup2(10, 1)                             = 1
   148  fcntl(10, F_GETFD)                      = 0x1 (flags FD_CLOEXEC)
   149  close(10)                               = 0

Вы можете видеть, как в строке 134 дескриптор 1( STDOUT) копируется в другой дескриптор с индексом, по крайней мере 10(это то, что F_DUPFDделает; он возвращает наименьший доступный дескриптор, начинающийся с заданного числа после дублирования на этот дескриптор). Также посмотрите, как в строке 137 результат open("/dev/null")(дескриптор 3) копируется в дескриптор 1( STDOUT). Наконец, в сети 147старое STDOUTсохраненное в дескрипторе 10копируется обратно в дескриптор 1( STDOUT). Чистый эффект состоит в том, чтобы изолировать изменение STDOUTна онлайн 144(что соответствует внутреннему exec >/dev/null).

DepressedDaniel
источник
Поскольку FD 1 перезаписывается FD 3 в строке 137, почему строка 141 не указывает 10 на / dev / null?
Джои Пабалинас
@JoeyPabalinas Строка 141 дублирует FD 1 (т.е. stdout) до следующего доступного дескриптора после 10 , который оказывается равным 11, как вы можете видеть в возвращаемом значении этого системного вызова. 10 просто жестко запрограммирован в bash, так что сохранение дескриптора bash не повлияет на однозначные дескрипторы, которыми вы можете манипулировать в своем скрипте exec.
ДепрессияДаниэль
Так что fcntl (1, F_DUPFD, 10) всегда будет ссылаться на STDOUT, независимо от того, куда в данный момент указывает FD 1?
Джои Пабалинас
@JoeyPabalinas Не уверен, что ваш вопрос. FD 1 IS STDOUT. Это одно и то же.
ДепрессияДаниэль
Добавлен полный вывод strace в мой оригинальный пост.
Джои Пабалинас
8

Разница между { exec >/dev/null; } >/dev/null; echo "Hi"и { exec >/dev/null; }; echo "Hi"заключается в том, что двойное перенаправление выполняется dup2(10, 1);перед закрытием fd 10, который является копией оригинала stdout, перед выполнением следующей команды ( echo).

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

Джули Пеллетье
источник
+1 за легкое объяснение разницы. В ответе AlexP отсутствует это объяснение.
Камиль Мачоровский