Проблема с перенаправлением вывода Bash

Я пытался удалить все строки файла, кроме последней, но следующая команда не сработала, хотя файл file.txt не пуст.

$cat file.txt |tail -1 > file.txt

$cat file.txt

Почему это так?

Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
10
0
5 917
11
Перейти к ответу Данный вопрос помечен как решенный

Ответы 11

Кажется, не нравится тот факт, что вы записываете его обратно с тем же именем файла. Если вы сделаете следующее, это сработает:

$cat file.txt | tail -1 > anotherfile.txt

Нет необходимости в предположениях типа «кажется»: нет никакой гарантии упорядочения между запуском компонентов конвейера, и перенаправления (таким образом, открытие anotherfile.txt в режиме усечения) происходят. Выполнение до (в данном случае tail; происходит ли это до выполнения cat не определено, но поскольку такой программе, как cat, требуется время для запуска, очень вероятно, что усечение anotherfile.txt уже произойдет до того, как cat завершит загрузку и будет готов открыть свой аргумент для ввода).

Charles Duffy 10.03.2016 22:13
Ответ принят как подходящий

Перенаправление из файла через конвейер обратно в тот же файл небезопасно; если file.txt перезаписывается оболочкой при настройке последней стадии конвейера до того, как tail начнет чтение с первой стадии, вы получите пустой вывод.

Вместо этого сделайте следующее:

tail -1 file.txt >file.txt.new && mv file.txt.new file.txt

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

tempfile = "$(mktemp file.txt.XXXXXX)"
chown --reference=file.txt -- "$tempfile"
chmod --reference=file.txt -- "$tempfile"
tail -1 file.txt >"$tempfile" && mv -- "$tempfile" file.txt

Другой подход (избегание временных файлов, если <<< неявно создает их на вашей платформе) заключается в следующем:

lastline = "$(tail -1 file.txt)"; cat >file.txt <<<"$lastline"

(Вышеупомянутая реализация специфична для bash, но работает в тех случаях, когда echo не работает, например, когда последняя строка содержит «--version»).

Наконец, можно использовать губку из Moreutils:

tail -1 file.txt | sponge file.txt

Обратите внимание, что tail принимает в качестве аргумента имя файла: "tail -1 file.txt> file.txt.new && mv file.txt.new file.txt"

Marcel Levy 23.09.2008 23:35

@Marcel Levy - совершенно верно, и таким образом он может работать более эффективно; обновлено.

Charles Duffy 24.09.2008 00:47

Уважаемый @CharlesDuffy, я хотел бы отметить, что ваш «безопасный» вариант требует, чтобы это делал пользователь root. Что касается того, насколько это безопасно, я могу сказать, что при доступе к "file.txt" недостаточно применить его разрешения и владельца. Учтите, что файл «file.txt» имеет права доступа 0666 и помещен в папку «~ / .ssh» с разрешениями 0700. В этом случае мое решение будет более безопасным, поскольку оно не будет открывать этот файл миру. Должен ли я из-за этого проголосовать против вашего ответа? ;)

ony 14.05.2013 17:02

@CharlesDuffy, о ... Я совершенно забыл, что вы можете потерять данные, если кто-то добавит одну строчку в этот файл, потому что вы открываете ее в течение всего времени tail -1 file.txt. В результате вы можете перезаписать существующий file.txt старым содержимым. Должен ли я из-за этого проголосовать против вашего ответа? ;)

ony 14.05.2013 17:15

@ony Использование chown потенциально полезно здесь, если файл принадлежит вторичной группе, и требует ли нет привилегий в этом случае.

Charles Duffy 14.05.2013 17:18

@ony Что касается состояния гонки - если бы этого можно было избежать, конечно, проголосуйте против меня. Я хотел бы услышать, как будет сделано это избегание (особенно учитывая, что sed -i, ed, ex и kin тоже не атомарны).

Charles Duffy 14.05.2013 17:19

@CharlesDuffy, для файла ~/.ssh/file.txt существует несколько уровней разрешений - сначала ~ (и все его родительские папки), затем .ssh (обычно это 0700) и затем file.txt. Вы копируете только разрешения для последнего элемента, что потенциально снижает уровень безопасности. В моем решении этого можно избежать, используя один и тот же файл (без перемещения / копирования / перезаписи). Так что этой проблемы можно избежать - это означает, что я могу проголосовать против? Конечно, у моего решения есть другая проблема - размер файла может отличаться от его содержимого, но это другая проблема. В моем первоначальном ответе я также избегаю проблем с разрешениями.

ony 14.05.2013 17:33

@ony Это серьезная проблема - временный файл должен создаваться в конечном месте, а не с использованием TEMPDIR. Внесение этого изменения; спасибо за указатель.

Charles Duffy 14.05.2013 17:38

я думаю, что TEMPFILE = $ (tempfile) намного проще ... конечно (нога в рот) не в каждом дистрибутиве есть это до сих пор? интересно, почему, если так "безопаснее" ..

osirisgothra 16.07.2014 19:12

@osirisgothra, действительно, tempfile зависит от дистрибутива. Недоступно в OS X, недоступно в самой последней версии Arch Linux и т. д. Кроме того, пользователю полезно знать программному обеспечению, с чем связан данный временный файл или каталог, что делает параметр шаблона ценной функцией.

Charles Duffy 16.07.2014 19:57

Как говорит Льюис Баумстарк, не нравится, что вы пишете в одно и то же имя файла.

Это связано с тем, что оболочка открывает файл «file.txt» и усекает его, чтобы выполнить перенаправление перед запуском «cat file.txt». Итак, вам нужно

tail -1 file.txt > file2.txt; mv file2.txt file.txt

tail -1 > file.txt перезапишет ваш файл, заставив cat прочитать пустой файл, потому что произойдет повторная запись до Выполняется любая из команд в вашем конвейере.

Перед запуском cat Bash уже открыл файл file.txt для записи, очищая его содержимое.

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

$cat file.txt | tail -1 >anotherfile.txt
$mv anotherfile.txt file.txt
or by using a utility like sponge from Moreutils:
$cat file.txt | tail -1 | sponge file.txt
This works because sponge waits until its input stream has ended before opening its output file.

Имеет ли cat какое-либо значение по сравнению с tail -1 file.txt? Это, безусловно, делает далеко менее эффективным, если ваш file.txt большой - если tail имеет дескриптор файла с прямым поиском, он может перейти прямо к последнему килобайту файла, прочитать только его и попытаться найти последнюю строку, сделав резервную копию, если последняя строка больше 1кб; если все, что у него есть, - это канал от cat, он должен прочитать файл с самого начала - независимо от его размера - чтобы добраться до конца.

Charles Duffy 21.07.2016 17:42

... tail -1 <file.txt также будет реальным дескриптором файла с возможностью поиска и на стандартном вводе (таким образом, идентичным аргументам командной строки, переданным вашему текущему подходу cat file.txt | tail -1).

Charles Duffy 21.07.2016 17:43

Когда вы отправляете свою командную строку в bash, она выполняет следующие действия:

  1. Создает канал ввода-вывода.
  2. Начинает "/ usr / bin / tail -1", чтение из канала и запись в file.txt.
  3. Запускает "/ usr / bin / cat file.txt", записывая в канал.

К тому времени, когда cat начинает читать, file.txt уже усечен словом tail.

Все это часть дизайна Unix и среды оболочки и восходит к исходной оболочке Bourne. «Это особенность, а не ошибка.

Указан ли порядок между (2) и (3)? У меня сложилось впечатление, что они происходят одновременно, что фактически приводит к возникновению гонки - хотя file.txt открывается для записи до того, как /usr/bin/tail будет запущен, поэтому усечение с высокой вероятностью выиграет гонку (поскольку /usr/bin/cat требует exec со всеми влияние на производительность компоновщика / загрузчика, что подразумевает).

Charles Duffy 21.07.2016 17:46

tmp = $ (хвост -1 файл.txt); echo $ tmp> file.txt;

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

wnoise 24.09.2008 00:31

Вы можете использовать sed для удаления всех строк, кроме последней, из файла:

sed -i '$!d' file
  • сообщает sed заменить файл на месте; в противном случае результат будет записан в STDOUT.
  • $ - это адрес, соответствующий последней строке файла.
  • d - команда удаления. В этом случае он отменяется !, поэтому все строки нет, соответствующие адресу, будут удалены.

Единственный прискорбный момент в том, что sed -i не совместим с POSIX, а является расширением GNU. Использование ed или ex для редактирования на месте позволит избежать этих недостатков.

Charles Duffy 14.05.2013 17:16

Это прекрасно работает в оболочке Linux:

replace_with_filter() {
  local filename = ""; shift
  local dd_output byte_count filter_status dd_status
  dd_output=$("$@" <"$filename" | dd conv=notrunc of = "$filename" 2>&1; echo "${PIPESTATUS[@]}")
  { read; read; read -r byte_count _; read filter_status dd_status; } <<<"$dd_output"
  (( filter_status > 0 )) && return "$filter_status"
  (( dd_status > 0 )) && return "$dd_status"
  dd bs=1 seek = "$byte_count" if=/dev/null of = "$filename"
}

replace_with_filter file.txt tail -1

Параметр «notrunc» dd используется для записи отфильтрованного содержимого обратно на место, в то время как dd требуется снова (со счетчиком байтов) для фактического усечения файла. Если размер нового файла больше или равен размеру старого файла, второй вызов dd не требуется.

Преимущества этого метода по сравнению с методом копирования файлов: 1) отсутствие необходимости в дополнительном дисковом пространстве, 2) более высокая производительность для больших файлов и 3) чистая оболочка (кроме dd).

Мне это нравится - это новаторская идея. Я только сейчас не поддерживаю его, потому что использование $FILTER противоречит mywiki.wooledge.org/BashFAQ/050 (использование разбиения строк для правильного формирования команды чрезвычайно чревато ошибками).

Charles Duffy 13.05.2013 18:14

Надеюсь, вы не возражаете против моих деспотичных правок; теперь это решение должно быть значительно более надежным.

Charles Duffy 14.05.2013 17:14

Я бы не назвал его replace_with_filter. Это будет работать только для фильтров, которые с каждым следующим блоком производят данные не больше, чем исходный блок. В противном случае информация, прочитанная фильтром, может быть перезаписана его выводом. Обратите внимание, что вместо последнего dd вы можете использовать truncate -s $byte_count "$filename".

ony 14.05.2013 20:00

@ony truncate не является стандартным для POSIX инструментом - он присутствует в современных coreutils, но существует множество платформ, где bash доступен, но его нет.

Charles Duffy 14.05.2013 21:23

Как раз в этом случае можно использовать

cat < file.txt | (rm file.txt; tail -1 > file.txt)
That will open "file.txt" just before connection "cat" with subshell in "(...)". "rm file.txt" will remove reference from disk before subshell will open it for write for "tail", but contents will be still available through opened descriptor which is passed to "cat" until it will close stdin. So you'd better be sure that this command will finish or contents of "file.txt" will be lost

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

ony 22.04.2013 13:54

Мне трудно не согласиться с (предполагаемым) утверждением, что следует избегать решения, в котором случаи отказа приводят к потере данных, даже если это предостережение четко обозначено; ненужная подоболочка - это вишенка на торте. А «наказали»? Действительно?

Charles Duffy 13.05.2013 18:12

@CharlesDuffy, "должен быть наказан" для анонима. Потому что отрицательный голос без каких-либо причин раздражает. Вы так не думаете? .. Что касается правильности, вы уверены, что для правильного ответа требуется высокая безопасность и надежность? Иногда можно чем-то пожертвовать ради архивации чего-то другого. Таким образом, с разных точек зрения вы можете по-разному относиться к решениям. Если вы спроецируете мой ответ на свое мнение, вы обнаружите, что предварительное условие для этого ответа не может быть выполнено, поэтому было бы неправильной логикой говорить, что это неверно.

ony 14.05.2013 16:57

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

Charles Duffy 14.05.2013 17:21

@CharlesDuffy, хорошо, это ваше предположение насчет того, кто проголосовал против. Но на самом деле причина может быть в другом. И вот в чем проблема.

ony 14.05.2013 19:34

Неправильно цитирую: «Если бы строители строили дома так, как разработчики создают программы, первый появившийся дятел уничтожит цивилизацию». Предположение, что люди используют излишне хрупкие подходы как нечто само собой разумеющееся (при отсутствии конкретной причины для иного), приводит к тому, что программное обеспечение и системы пронизаны неожиданными и недокументированными режимами отказов и, таким образом, увековечивают проблему.

Charles Duffy 14.05.2013 19:51

@CharlesDuffy, факт замены содержимого файла его последней строкой - это уже неправильный путь. Это должны быть либо два отдельных файла (если кому-то нужно все содержимое), либо только одна строка в этом файле (т.е. одна строка, а не все содержимое), чтобы не было необходимости делать с ним tail -1. И я почти уверен, что разнообразие вариантов здесь связано с тем, что это больше похоже на головоломку.

ony 14.05.2013 21:00

Оперативное редактирование - это то, что делают люди. tail -1 может быть не особенно хорошим вариантом использования (на самом деле, это точно не так), но любой, кто имеет карьеру в системном администрировании, будет с нетривиальной частотой использовать методы, аналогичные тем, которые здесь встречаются в реальных производственных средах и сценариях. Таким образом, нам надлежит - «головоломка» или нет - поощрять принятие хороших практик и препятствовать принятию плохих.

Charles Duffy 14.05.2013 21:28

echo "$(tail -1 file.txt)" > file.txt

Что будет, если последняя строка содержит только -n? Что, если он содержит литералы с обратной косой чертой и ваш echo совместим с расширениями XSI POSIX (которые требуют, чтобы любые escape-последовательности, которые эти литералы могли образовывать, учитывались даже без чего-либо, похожего на -e)? Было бы безопаснее использовать printf '%s\n' "$(tail -1 file.txt)", чем полагаться на эхо

Charles Duffy 21.07.2016 17:47

Другие вопросы по теме