Перенаправление ввода-вывода и команда head

9

.hgignoreСегодня я пытался быстро отредактировать файл из оболочки Cygwin bash и добавил строку, которая была ошибкой. Я не уверен, что это был лучший способ сделать это, но я быстро подумал об использовании head -1 .hgignoreдля удаления ошибочной строки (ранее у меня была только одна строка в файле). Конечно, при выполнении он выдает первую строку в качестве единственного вывода.

Но когда я попытался перенаправить вывод и переписать файл, используя head -1 .hgignore > .hgignore, файл был пустым. Почему это происходит? Если я попытаюсь добавить вместо этого, head -1 .hgignore >> .hgignoreон добавляется правильно, но это, очевидно, не желаемый результат. Почему усечающее перенаправление не работает в этом случае?

voithos
источник

Ответы:

10

Когда оболочка получает командную строку, например: command > file.outоболочка сама открывает (и, возможно, создает) файл с именем file.out. Оболочка устанавливает дескриптор файла 0 равным дескриптору файла, который он получил при открытии. Вот как работает перенаправление ввода / вывода: каждый процесс знает о дескрипторах файлов 0, 1 и 2.

Самое сложное в этом - как открыть file.out. Большую часть времени вы хотите file.outоткрыть для записи со смещением 0 (то есть усеченным), и это то, что оболочка сделала для вас. Он обрезал .hgignore, открыл его для записи, дублировал дескриптор файла до 0, а затем выполнил head. Мгновенное засорение файлов.

В bash shell вы делаете это, set noclobberчтобы изменить это поведение.

Брюс Эдигер
источник
Ага, понятно Я думал, что оболочка обрезает файл перед запуском команды, но я не знал почему. Спасибо за объяснение!
Voithos
10

Я думаю, что Брюс отвечает на то, что здесь происходит с конвейером оболочки.

Одна из моих любимых маленьких утилит - spongeкоманда от moreutils . Он решает именно эту проблему, «впитывая» все доступные входные данные, прежде чем открыть целевой выходной файл и записать данные. Это позволяет вам писать конвейеры точно так, как вы ожидали:

$ head -1 .hgignore | sponge .hgignore

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

$ head -1 .hgingore > .hgignore.tmp
$ mv .hgignore{.tmp,}
Калеб
источник
Глядя на это несколько лет спустя, мне пришла в голову мысль: не могли бы мы просто сделать head -1 .hgignore | tee .hgignore? teeнаходится в coreutils, и как перк / побочный эффект, это также пишет в STDOUT
voithos
@voithos Насколько мне известно, teeоткрывается и усекается файл, в который он записывается, когда он создается, как и все остальное, поэтому он не решает главную проблему условия гонки при чтении содержимого файла до того, как вы усекаете его с записью.
Калеб
Вы подняли вопрос, о котором я на самом деле не знал, а именно, что конвейерные команды запускаются сразу, а не последовательно. Это точно? Я, однако, проверил это и, tee кажется, сделал желаемую вещь. У меня есть версия 8.13на моей машине.
voithos
1
@voithos Команды Yes в трубопроводе и все задействованные каналы ввода / вывода запускаются в обратном порядке, поэтому конвейер готов к приему данных, когда первый из них начнет их выдавать. Я подозреваю, что ваш тест некорректен, потому что вы, вероятно, использовали слишком маленький кусок данных, и он целиком кэшировался в буфере чтения, прежде чем он вам понадобился. teeПрограмма будет укоротить ваши файлы, он не настроен на двойной буфер них.
Калеб
3

В

head -n 1 file > file

fileобрезается до headзапуска, но если вы напишите это:

head -n 1 file 1<> file

это не так, как fileоткрывается в режиме чтения-записи. Однако, когда он headзавершает запись, он не усекает файл, поэтому строка выше будет неактивной ( headпросто перезапишет первую строку поверх себя и оставит остальные нетронутыми).

Тем не менее, после того, как headвернулось и когда fdон все еще открыт, вы можете вызвать другую команду, которая выполняет truncate.

Например:

{ head -n 1 file; perl -e 'truncate STDOUT, tell STDOUT'; } 1<> file

Здесь важно то truncate, что выше, headпросто перемещает курсор для fd 1 внутри файла сразу после первой строки. Он переписывает первую строку, которая нам не нужна, но это не вредно.

С головой POSIX мы могли бы уйти без переписывания первой строки:

{ head -n 1 > /dev/null
  perl -e 'truncate STDIN, tell STDIN'
} <> file

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

В качестве альтернативы вы можете использовать команду оболочки readв этом случае:

{ read -r dummy; perl -e 'truncate STDIN, tell STDIN'; } <> file
Стефан Шазелас
источник
1
Стефан, знаете ли вы стандартную команду или команду coreutils, которая может обрезать STDINаналогично тому, что вы выполнили, используя perlвыше
iruvar
2
@ 1_CR, нет. ddможет усечь любое произвольное абсолютное смещение в файле, хотя. Таким образом, вы можете определить смещение в байтах второй строки и отсечь оттуда с помощьюdd bs=1 seek="$offset" of=file
Стефан Шазелас
1

Решение настоящего мужчины

ed .hgignore
$d
wq

или как однострочник

printf '%s\n' '$d' 'wq' | ed .hgignore

Или с GNU sed:

sed -i '$d' .hgignore

(Нет, я шучу. Я бы использовал интерактивный редактор. vi .hgignore GddZZ)

Жиль "ТАК - перестань быть злым"
источник
Я задавался вопросом, есть ли преимущество использования :wqболее ZZ?
Voithos
Кроме того, :xэто то, что мои пальцы делают автоматически
Гленн Джекман
и так ZQже, как:q!
Гленн Джекман
ZZ и: x пишут только если есть что написать ...: w всегда fsyncs файл на диск независимо от того, нужен ли он. Я использую: XA, потому что я использую вкладки.
ксенотеррацид
1

Вы можете использовать Vim в режиме Ex:

ex -sc '2,d|x' .hgignore
  1. 2, выберите строки 2 до конца

  2. d удалять

  3. x сохранить и закрыть

Стивен Пенни
источник
0

Для редактирования файлов на месте вы также можете использовать прием с открытым файлом, как показано Юргеном Хетцелем в перенаправлении вывода из sed 's / c / d /' myFile в myFile .

exec 3<.hgignore
rm .hgignore  # prevent open file from being truncated
head -1 <&3 > .hgignore

ls -l .hgignore  # note that permissions may have changed
dan55
источник
2
И только после того, как rm .hgignoreваша сила выходит из строя, отнимает часы тяжелой работы. Хорошо, это не имеет значения .hgignore, но зачем вам делать что-то сложное в любом случае? Таким образом, мой недостаток: технически правильная, но очень плохая идея.
Жиль "ТАК - перестань быть злым"
@ Жиль, может быть, не очень хорошая идея, но это, например, то, что perl -i(для редактирования на месте) делает, и я не удивлюсь, если некоторые реализации sed -iсделали это также (хотя последняя версия GNU, sedкажется, не делает ).
Стефан Шазелас