Почему ls> ls.out вызывает включение ls.out в список имен?

26

Почему $ ls > ls.outls.out включается в список имен файлов в текущем каталоге? Почему это было выбрано? Почему не иначе?

Эдвард Торвальдс
источник
3
вероятно, потому что он сначала создает файл ls.out, а затем записывает в него вывод
Дмитрий Подборский
1
Если вы хотите избежать включения, вы всегда можете сохранить выходной файл в другом каталоге. Например, вы можете выбрать родительский каталог (при условии, что вы не в корне файловой системы) с помощью ls > ../ls.out
Elder Geek

Ответы:

36

При оценке команды >перенаправление сначала разрешается: поэтому к тому времени, как lsзапускается, выходной файл уже создан.

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

$ echo foo >bar
$ cat bar
foo
$ <bar cat >bar
$ cat bar
$ 

Уловки, чтобы избежать этого:

  • <<<"$(ls)" > ls.out (работает для любой команды, которая должна быть запущена до разрешения перенаправления)

    Подстановка команд выполняется до оценки внешней команды, поэтому lsвыполняется до ls.outсоздания:

    $ ls
    bar  foo
    $ <<<"$(ls)" > ls.out
    $ cat ls.out 
    bar
    foo
  • ls | sponge ls.out (работает для любой команды, которая должна быть запущена до разрешения перенаправления)

    spongeзаписывает в файл только после завершения остальной части канала, поэтому lsвыполняется до ls.outсоздания ( spongeпредоставляется с moreutilsпакетом):

    $ ls
    bar  foo
    $ ls | sponge ls.out
    $ cat ls.out 
    bar
    foo
  • ls * > ls.out(работает для ls > ls.outконкретного случая)

    Расширение имени файла выполняется до разрешения перенаправления, поэтому lsбудет работать с его аргументами, которые не будут содержать ls.out:

    $ ls
    bar  foo
    $ ls * > ls.out
    $ cat ls.out 
    bar
    foo
    $

О том, почему переназначения разрешаются до программы / скрипта / все , что бежать, я не вижу причин , почему конкретные это обязательное сделать это, но я вижу две причины , почему это лучше сделать так:

  • если вы не перенаправите STDIN заранее, программа / скрипт / все остальное будет удерживаться до перенаправления STDIN;

  • предварительное перенаправление STDOUT не обязательно должно делать буфер оболочки обязательным для вывода программы / скрипта / что угодно, пока не будет перенаправлен STDOUT;

Таким образом, пустая трата времени в первом случае и пустая трата времени и памяти во втором случае.

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

кос
источник
1
Обратите внимание, что во время перенаправления оболочка фактически не касается данных (при перенаправлении ввода или перенаправления вывода). Он просто открывает файл и передает дескриптор файла в программу.
Питер Грин
11

От man bash:

Переквалификация

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

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

Чтобы избежать файла, я предлагаю вам перенаправить вывод сначала в именованный канал, а затем в файл. Обратите внимание на использование &для возврата контроля над терминалом пользователю

DIR:/xieerqi
skolodya@ubuntu:$ mkfifo /tmp/namedPipe.fifo                                                                         

DIR:/xieerqi
skolodya@ubuntu:$ ls > /tmp/namedPipe.fifo &
[1] 14167

DIR:/xieerqi
skolodya@ubuntu:$ cat /tmp/namedPipe.fifo > ls.out

Но почему?

Подумайте об этом - где будет выход? Программа имеет такие функции , как printf, sprintf, puts, которые все по умолчанию идти к stdout, но может их выход пропадут в файл , если файл не существует , в первую очередь? Это как вода. Можете ли вы получить стакан воды, не ставя стакан под кран сначала?

Сергей Колодяжный
источник
10

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

Это потому, что "все это файл" в нашем мире. Вывод на экран - SDOUT (он же дескриптор файла 1). Для приложения, чтобы записать в терминал, он открывает fd1 и пишет в него как файл.

Когда вы перенаправляете вывод приложения в оболочку, вы изменяете fd1, чтобы он фактически указывал на файл. Когда вы передаете по каналу, вы изменяете STDOUT одного приложения, чтобы оно стало STDIN другого (fd0).


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

strace sh -c "ls > ls.out" 2> strace.out

Внутри strace.outмы можем увидеть следующие основные моменты:

open("ls.out", O_WRONLY|O_CREAT|O_TRUNC, 0666) = 3

Это открывается ls.outкак fd3. Пиши только. Усекает (перезаписывает), если существует, в противном случае создает.

fcntl(1, F_DUPFD, 10)                   = 10
close(1)                                = 0
fcntl(10, F_SETFD, FD_CLOEXEC)          = 0
dup2(3, 1)                              = 1
close(3)                                = 0

Это немного жонглирования. Мы отключаем STDOUT (fd1) от fd10 и закрываем его. Это потому, что мы не выводим что-либо в реальный STDOUT с помощью этой команды. Он заканчивается дублированием дескриптора записи ls.outи закрытием исходного.

stat("/opt/wine-staging/bin/ls", 0x7ffc6bf028c0) = -1 ENOENT (No such file or directory)
stat("/home/oli/bin/ls", 0x7ffc6bf028c0) = -1 ENOENT (No such file or directory)
stat("/usr/local/sbin/ls", 0x7ffc6bf028c0) = -1 ENOENT (No such file or directory)
stat("/usr/local/bin/ls", 0x7ffc6bf028c0) = -1 ENOENT (No such file or directory)
stat("/usr/sbin/ls", 0x7ffc6bf028c0)    = -1 ENOENT (No such file or directory)
stat("/usr/bin/ls", 0x7ffc6bf028c0)     = -1 ENOENT (No such file or directory)
stat("/sbin/ls", 0x7ffc6bf028c0)        = -1 ENOENT (No such file or directory)
stat("/bin/ls", {st_mode=S_IFREG|0755, st_size=110080, ...}) = 0

Это он ищет исполняемый файл. Урок, возможно, чтобы не иметь длинный путь;)

clone(child_stack=0, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD, child_tidptr=0x7f0961324a10) = 31933
wait4(-1, [{WIFEXITED(s) && WEXITSTATUS(s) == 0}], 0, NULL) = 31933
--- SIGCHLD {si_signo=SIGCHLD, si_code=CLD_EXITED, si_pid=31933, si_status=0, si_utime=0, si_stime=0} ---
rt_sigreturn()                          = 31933
dup2(10, 1)                             = 1
close(10)                               = 0

Затем команда выполняется, и родитель ждет. Во время этой операции любой STDOUT будет фактически сопоставлен с дескриптором открытого файла ls.out. Когда дочерний процесс выдает сообщение SIGCHLD, он сообщает родительскому процессу, что он завершен, и может возобновить его. Это заканчивается немного больше жонглирования и закрытия ls.out.

Почему так много жонглирования? Нет, я тоже не совсем уверен.


Конечно, вы можете изменить это поведение. Вы можете буферизовать в памяти что-то вроде этого, spongeи это будет невидимым из исходящей команды. Мы по-прежнему влияем на файловые дескрипторы, но не видимыми для файловой системы.

ls | sponge ls.out
Оли
источник
6

Также есть хорошая статья о реализации перенаправления и конвейерных операторов в оболочке . Который показывает, как перенаправление может быть реализовано так, чтобы $ ls > ls.outмогло выглядеть так:

main(){
    close(1); // Release fd no - 1
    open("ls.out", "w"); // Open a file with fd no = 1
    // Child process
    if (fork() == 0) {
        exec("ls"); 
    }
}
Дмитрий Подборский
источник