Linux: Как использовать файл в качестве ввода и вывода одновременно?

55

Я просто запустил следующее в bash:

uniq .bash_history > .bash_history

и мой файл истории оказался совершенно пустым.

Я думаю, мне нужен способ прочитать весь файл перед записью в него. Как это сделать?

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

MilliaLover
источник
Это потому, что файлы открываются справа налево. См. Также stackoverflow.com/questions/146435/…
WheresAlice
Вы должны записать вывод в новый файл в том же каталоге и переименовать его поверх старого файла. Любой другой подход может привести к потере ваших данных, если они будут прерваны на полпути. Некоторые инструменты могут скрыть этот шаг от вас.
Касперд
Или bashне будет помещать последовательные дубликаты в свою историю, если вы установите HISTCONTROL для включения ignoredups; см. справочную страницу.
dave_thompson_085
пожалуйста, подумайте об изменении ответа на этот. serverfault.com/a/557566/130392
23inhouse

Ответы:

49

Я рекомендую использовать spongeот moreutils . Из справочной страницы:

DESCRIPTION
  sponge  reads  standard  input  and writes it out to the specified file. Unlike
  a shell redirect, sponge soaks up all its input before opening the output file.
  This allows for constructing pipelines that read from and write to the same 
  file.

Чтобы применить это к вашей проблеме, попробуйте:

uniq .bash_history | sponge .bash_history
jldugger
источник
6
Это как кошка, но с возможностями для сосания: D
MilliaLover
77

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

echo "$(uniq .bash_history)" > .bash_history

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

Харт Симха
источник
15
Я обычно не фанат ответов, которые углубляют древний вопрос, у которого уже есть действительный, принятый ответ - но это изящно, хорошо написано и дает веские аргументы в пользу его необходимости (облегченная среда); для меня это действительно добавляет что-то к существующему набору ответов. Добро пожаловать в SF, Харт (вы были здесь месяц, но я думаю, что это ваша первая важная публикация). Я надеюсь прочитать больше ответов от вас, как этот!
MadHatter
4
Это лучшее решение. Мне пришлось использовать подоболочку $()вместо кавычек из-за некоторых проблем с выходом из строя.
CMCDragonkai
3
Мне интересно, если это решение масштабируется до больших файлов, скажем ... 20 или 50 ГБ.
Амит Найду
1
Это действительно должен быть ответ.
maxywb
1
Я использовал этот ответ, чтобы сделать echo "$(fmt -p '# ' -w 50 readme.txt)" > readme.txtсегодня. Долго искал вокруг элегантное решение. Большое спасибо, @Hart Simha!
Шредалерт
12

Проблема в том, что ваша оболочка настраивает конвейер команд перед запуском команд. Дело не в «вводе и выводе», а в том, что содержимое файла уже удалено до того, как uniq даже запустится. Это выглядит примерно так:

  1. Оболочка открывает >выходной файл для записи, обрезая его
  2. Оболочка настроена на использование файлового дескриптора 1 (для stdout) для этого вывода
  3. Оболочка выполняет uniq, возможно что-то вроде execlp ("uniq", "uniq", ".bash_history", NULL)
  4. uniq запускается, открывает .bash_history и там ничего не находит

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

Фил П
источник
10

Еще один трюк, чтобы сделать это без использования sponge, это следующая команда:

{ rm .bash_history && uniq > .bash_history; } < .bash_history

Это один из читов, описанных в отличной статье «Редактирование файлов на месте» на backreference.org.

Он в основном открывает файл для чтения, затем «удаляет» его. Однако на самом деле он не удален: на него указывает дескриптор открытого файла, и пока он остается открытым, файл все еще существует. Затем он создает новый файл с тем же именем и записывает в него уникальные строки.

Недостаток этого решения: если uniqпо какой-то причине произойдет сбой, ваша история исчезнет.

SCY
источник
6

использовать губку от moreutils

uniq .bash_history | sponge .bash_history
Джастин
источник
3

Этот sedскрипт удаляет соседние дубликаты. С -iопцией, он делает модификацию на месте. Это из sed infoфайла:

sed -i 'h;:b;$b;N;/^\(.*\)\n\1$/ {g;bb};$b;P;D' .bash_history
Деннис Уильямсон
источник
Сед все еще использует временный файл, добавил ответ с straceиллюстрацией (не то, чтобы это действительно имело значение) :-)
Кайл Брандт
3
@Kyle: правда, но "с глаз долой, с ума". Лично я бы использовал явный временный файл, поскольку что-то вроде этого process input > tmp && mv tmp inputгораздо проще и более читабельно, чем использование sedхитрости просто для того, чтобы избежать временного файла, и он не будет перезаписывать мой оригинал, если он потерпит неудачу (я не знаю, будет ли sed -iнеудача изящной - я хотел бы думаю, что будет, хотя). Кроме того, есть много вещей, которые вы можете сделать с помощью метода output-to-temp-file, который нельзя сделать на месте без чего-то более сложного, чем этот sedскрипт. Я знаю, что вы знаете все это, но это может принести пользу некоторым зрителям.
Деннис Уильямсон
3

Как интересный трюк, sed также использует временный файл (это только для вас):

$ strace sed -i 's/foo/bar/g' foo    
open("foo", O_RDONLY|O_LARGEFILE)       = 3
...
open("./sedPmPv9z", O_RDWR|O_CREAT|O_EXCL|O_LARGEFILE, 0600) = 4
...
read(3, "foo\n"..., 4096)               = 4
write(4, "bar\n"..., 4)                 = 4
read(3, ""..., 4096)                    = 0
close(3)                                = 0
close(4)                                = 0
rename("./sedPmPv9z", "foo")            = 0
close(1)                                = 0
close(2)                                = 0

Описание:
временный файл ./sedPmPv9zстановится Fd 4, и fooфайлы становятся Fd 3. Операций чтения на дескрипторе 3, и запись на Fd 4 (временный файл). Затем файл foo перезаписывается временным файлом в вызове переименования.

Кайл Брандт
источник
1

Другое решение:

uniq file 1<> líneas.txt
Антонио Леброн
источник
0

Временный файл - это почти то же самое, если только рассматриваемая команда не поддерживает редактирование на месте ( uniqнет - некоторые sedделают do ( sed -i)).

Дуглас Лидер
источник
0

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

ex -sc '%!uniq' -cx .bash_history
  1. % выбрать все строки

  2. ! Команда Run

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

Стивен Пенни
источник
-1

Вы также можете использовать tee, используя вывод uniq в качестве входных данных:

uniq .bash_history | tee .bash_history
Сол Варгас
источник
Нет, вы не можете сделать это askubuntu.com/a/752451
Стивен Пенни