Как я могу использовать файл в команде и перенаправить вывод в тот же файл, не усекая его?


98

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

grep -v 'seg[0-9]\{1,\}\.[0-9]\{1\}' file_name > file_name

однако, когда я это делаю, я получаю пустой файл. есть идеи?


Ответы:


85

Вы не можете этого сделать, потому что bash сначала обрабатывает перенаправления, а затем выполняет команду. Итак, к тому времени, когда grep посмотрит на file_name, он уже пуст. Однако вы можете использовать временный файл.

#!/bin/sh
tmpfile=$(mktemp)
grep -v 'seg[0-9]\{1,\}\.[0-9]\{1\}' file_name > ${tmpfile}
cat ${tmpfile} > file_name
rm -f ${tmpfile}

таким образом, подумайте об использовании mktempдля создания tmpfile, но обратите внимание, что это не POSIX.


47
Причина, по которой вы не можете этого сделать: bash сначала обрабатывает перенаправления, а затем выполняет команду. Итак, к тому времени, когда grep посмотрит на file_name, он уже пуст.
Гленн Джекман

1
@glennjackman: под "перенаправлением процессов вы имеете в виду, что в случае> он открывает файл и очищает его, а в случае >> он только открывает его"?
Разван,

2
да, но обратите внимание, что в этой ситуации >перенаправление откроет файл и усечет его перед запуском оболочки grep.
Гленн Джекман

1
См. Мой ответ, если вы не хотите использовать временный файл, но, пожалуйста, не голосуйте за этот комментарий.
Зак Моррис

Вместо этого следует принять ответ с помощью spongeкоманды .
vlz

98

Используйте губку для такого рода задач. Его часть moreutils.

Попробуйте эту команду:

 grep -v 'seg[0-9]\{1,\}\.[0-9]\{1\}' file_name | sponge file_name

4
Спасибо за ответ. В качестве, возможно, полезного дополнения, если вы используете homebrew на Mac, можете использовать brew install moreutils.
Энтони Паноццо

2
Или sudo apt-get install moreutilsв системах на основе Debian.
Иона

3
Черт! Спасибо, что познакомили меня с moreutils =) там несколько хороших программ!
Пользователь сети,

Большое спасибо, moreutils за спасение! губка как босс!
aqquadro,

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

19

Вместо этого используйте sed:

sed -i '/seg[0-9]\{1,\}\.[0-9]\{1\}/d' file_name

1
Просто замечу, что iirc -i- это только расширение GNU.
c00kiemon5ter

4
В * BSD (и, следовательно, также в OSX) вы можете сказать, -i ''что расширение не является строго обязательным, но для этой -iопции требуется некоторый аргумент.
Tripleee

16

попробуйте этот простой

grep -v 'seg[0-9]\{1,\}\.[0-9]\{1\}' file_name | tee file_name

На этот раз ваш файл не будет пустым :), и ваш результат также будет распечатан на вашем терминале.


1
Мне нравится это решение! И если вы не хотите, чтобы он печатался в терминале, вы все равно можете перенаправить вывод в /dev/nullили аналогичные места.
Frozn

4
Здесь также очищается содержимое файла. Это из-за разницы между GNU / BSD? Я использую macOS ...
ssc

7

Вы не можете использовать оператор перенаправления ( >или >>) для того же файла, потому что он имеет более высокий приоритет и будет создавать / усекать файл до того, как команда будет запущена. Чтобы избежать этого, вы должны использовать соответствующие инструменты , такие как tee, sponge, sed -iили любой другой инструмент , который может записывать результаты в файл (например sort file -o file).

По сути, перенаправление ввода в тот же исходный файл не имеет смысла, и вы должны использовать для этого соответствующие редакторы на месте, например редактор Ex (часть Vim):

ex '+g/seg[0-9]\{1,\}\.[0-9]\{1\}/d' -scwq file_name

где:

  • '+cmd'/ -c- запустить любую команду Ex / Vim
  • g/pattern/d- удалить строки, соответствующие шаблону, используя global ( help :g)
  • -s- беззвучный режим ( man ex)
  • -c wq- выполнить :writeи :quitкоманды

Вы можете использовать sedдля достижения того же (как уже было показано , в других ответах), однако на месте ( -i) является нестандартным расширением FreeBSD (может работать по- разному между Unix / Linux) , и в основном это s Tream ред itor, а не редактор файлов . См .: Есть ли практическое применение в режиме Ex?


6

Альтернатива с одним лайнером - установите содержимое файла как переменную:

VAR=`cat file_name`; echo "$VAR"|grep -v 'seg[0-9]\{1,\}\.[0-9]\{1\}' > file_name

4

Поскольку этот вопрос является лучшим результатом в поисковых системах, вот однострочный запрос на основе https://serverfault.com/a/547331, который использует подоболочку вместо sponge(что часто не является частью стандартной установки, такой как OS X) :

echo "$(grep -v 'seg[0-9]\{1,\}\.[0-9]\{1\}' file_name)" > file_name

Общий случай:

echo "$(cat file_name)" > file_name

Изменить, в приведенном выше решении есть некоторые предостережения:

  • printf '%s' <string>следует использовать вместо, echo <string>чтобы файлы, содержащие -n, не вызывали нежелательного поведения.
  • Подстановка команд удаляет завершающие символы новой строки ( это ошибка / особенность оболочек, таких как bash ), поэтому мы должны добавить постфиксный символ, например, xк выходным данным, и удалить его снаружи через расширение параметров временной переменной, например ${v%x}.
  • Использование временной переменной $vвытесняет значение любой существующей переменной $vв текущей среде оболочки, поэтому мы должны вложить все выражение в круглые скобки, чтобы сохранить предыдущее значение.
  • Еще одна ошибка / особенность оболочек, таких как bash, заключается в том, что подстановка команд удаляет непечатаемые символы, например, nullиз вывода. Я проверил это, позвонив dd if=/dev/zero bs=1 count=1 >> file_nameи просмотрев в шестнадцатеричном формате с помощью cat file_name | xxd -p. Но echo $(cat file_name) | xxd -pраздевается. Таким образом , как указал Линч, этот ответ не следует использовать для двоичных файлов или чего-либо, содержащего непечатаемые символы .

Общее решение (albiet немного медленнее, требует больше памяти и по-прежнему удаляет непечатаемые символы):

(v=$(cat file_name; printf x); printf '%s' ${v%x} > file_name)

Тест с https://askubuntu.com/a/752451 :

printf "hello\nworld\n" > file_uniquely_named.txt && for ((i=0; i<1000; i++)); do (v=$(cat file_uniquely_named.txt; printf x); printf '%s' ${v%x} > file_uniquely_named.txt); done; cat file_uniquely_named.txt; rm file_uniquely_named.txt

Следует напечатать:

hello
world

В то время как вызов cat file_uniquely_named.txt > file_uniquely_named.txtв текущей оболочке:

printf "hello\nworld\n" > file_uniquely_named.txt && for ((i=0; i<1000; i++)); do cat file_uniquely_named.txt > file_uniquely_named.txt; done; cat file_uniquely_named.txt; rm file_uniquely_named.txt

Печатает пустую строку.

Я не тестировал это на больших файлах (вероятно, более 2 или 4 ГБ).

Я позаимствовал этот ответ у Харта Симха и Кос .


2
Конечно, с большими файлами работать не будет. Это не может быть хорошим решением или работать постоянно. Что происходит, так это то, что bash сначала выполняет команду, а затем загружает стандартный вывод catи помещает его в качестве первого аргумента echo. Конечно, непечатаемые переменные не будут выводиться должным образом и испортят данные. Не пытайтесь перенаправить файл обратно на себя, это не может быть хорошим.
Lynch

1

Также есть ed(как альтернатива sed -i):

# cf. http://wiki.bash-hackers.org/howto/edit-ed
printf '%s\n' H 'g/seg[0-9]\{1,\}\.[0-9]\{1\}/d' wq |  ed -s file_name

1

Вы можете сделать это, используя подстановку процесса .

Это немного похоже на взлом, поскольку bash открывает все каналы асинхронно, и мы должны обойти это, используя sleepYMMV.

В вашем примере:

grep -v 'seg[0-9]\{1,\}\.[0-9]\{1\}' file_name > >(sleep 1 && cat > file_name)
  • >(sleep 1 && cat > file_name) создает временный файл, который получает вывод от grep
  • sleep 1 задержки на секунду, чтобы дать grep время проанализировать входной файл
  • наконец cat > file_nameзаписывает вывод

1

Вы можете использовать slurp с POSIX Awk:

!/seg[0-9]\{1,\}\.[0-9]\{1\}/ {
  q = q ? q RS $0 : $0
}
END {
  print q > ARGV[1]
}

пример


1
Возможно, следует отметить, что «slurp» означает «прочитать весь файл в память». Если у вас большой входной файл, возможно, вы захотите этого избежать.
tripleee

1

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

exec 3<file ; rm file; COMMAND <&3 >file ;  exec 3>&-

Или построчно, чтобы лучше понять:

exec 3<file       # open a file descriptor reading 'file'
rm file           # remove file (but fd3 will still point to the removed file)
COMMAND <&3 >file # run command, with the removed file as input
exec 3>&-         # close the file descriptor

Это по-прежнему рискованно, потому что, если COMMAND не работает должным образом, вы потеряете содержимое файла. Это можно уменьшить, восстановив файл, если КОМАНДА возвращает ненулевой код выхода:

exec 3<file ; rm file; COMMAND <&3 >file || cat <&3 >file ; exec 3>&-

Мы также можем определить функцию оболочки, чтобы упростить ее использование:

# Usage: replace FILE COMMAND
replace() { exec 3<$1 ; rm $1; ${@:2} <&3 >$1 || cat <&3 >$1 ; exec 3>&- }

Пример :

$ echo aaa > test
$ replace test tr a b
$ cat test
bbb

Также обратите внимание, что при этом будет сохранена полная копия исходного файла (до закрытия третьего файлового дескриптора). Если вы используете Linux, и файл, который вы обрабатываете, слишком велик, чтобы дважды поместиться на диске, вы можете проверить этот скрипт, который будет перенаправлять файл в указанную команду блок за блоком, освобождая уже обработанные блоки. Как всегда, прочтите предупреждения на странице использования.


0

Попробуй это

echo -e "AAA\nBBB\nCCC" > testfile

cat testfile
AAA
BBB
CCC

echo "$(grep -v 'AAA' testfile)" > testfile
cat testfile
BBB
CCC

Может быть полезно краткое объяснение или даже комментарии.
Rich

Я думаю, это работает, потому что экстраполяция строк выполняется перед оператором перенаправления, но я точно не знаю
Виктор Пупкин

0

Следующее сделает то же самое, spongeно не требует moreutils:

    shuf --output=file --random-source=/dev/zero 

Эта --random-source=/dev/zeroчасть пытается shufделать свое дело, не тасуя вообще, поэтому она буферизует ваш ввод, не изменяя его.

Однако это правда, что использование временного файла лучше всего по соображениям производительности. Итак, вот функция, которую я написал, которая сделает это за вас в обобщенном виде:

# Pipes a file into a command, and pipes the output of that command
# back into the same file, ensuring that the file is not truncated.
# Parameters:
#    $1: the file.
#    $2: the command. (With $3... being its arguments.)
# See https://stackoverflow.com/a/55655338/773113

function siphon
{
    local tmp=$(mktemp)
    local file="$1"
    shift
    $* < "$file" > "$tmp"
    mv "$tmp" "$file"
}

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.