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

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

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

$cat file.txt

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

11 ответов


перенаправление из файла через конвейер обратно в тот же файл небезопасно; если 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

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

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

прежде чем " cat "будет выполнен, Bash уже открыл" файл.txt ' для написания, очистки его содержимого.

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

$cat file.txt | tail -1 >anotherfile.txt
$mv anotherfile.txt file.txt
или с помощью утилиты, такой как sponge from moreutils:
$cat file.txt | tail -1 | sponge file.txt
Это работает, потому что sponge ждет, пока его входной поток не закончится, прежде чем открыть его выходной файл.

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

  1. создает канал ввода-вывода.
  2. запускает "/ usr/bin / tail -1", чтение из трубы и запись в файл.формат txt.
  3. Starts " /usr/bin / cat файл.txt", надпись на трубе.

к тому времени, как "кошка" начнет читать, " файл.тхт уже обрезан "хвост".

Это все часть дизайна Unix и среды оболочки и возвращает все путь к оригинальной оболочке Борна. Это особенность, а не ошибка.


tmp=$(файл tail -1.txt); echo $tmp > файл.txt;


это хорошо работает в оболочке 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

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

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


как Льюис Baumstark говорит, ему не нравится то, что ты пишешь с тем же именем.

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

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

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

только для этого случая можно использовать

cat < file.txt | (rm file.txt; tail -1 > file.txt)
Что открыть файл".txt "непосредственно перед подключением" cat " с подсхемой в "(...)". - rm-файл.txt " удалит ссылку с диска, прежде чем subshell откроет его для записи для "хвоста", но содержимое будет по-прежнему доступно через открытый дескриптор, который передается "cat", пока он не закроет stdin. Поэтому вам лучше быть уверенным, что эта команда завершит или содержимое "файла.txt" будет потеряна

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

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

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