Як я можу використовувати файл у команді та перенаправляти вихід на той самий файл, не скорочуючи його?


98

В основному я хочу взяти як вхідний текст з файлу, видалити рядок із цього файлу і відправити вихідні дані назад у той самий файл. Щось у цьому напрямку, якщо це робить це яснішим.

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

однак, коли я це роблю, у підсумку отримую порожній файл. Будь-які думки?


Відповіді:


84

Ви не можете цього зробити, оскільки bash спочатку обробляє перенаправлення, а потім виконує команду. Отже, на момент, коли grep переглядає ім’я_файлу, воно вже порожнє. Ви можете використовувати тимчасовий файл.

#!/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 переглядає ім’я_файлу, воно вже порожнє.
glenn jackman

1
@glennjackman: під "перенаправленням процесів ви маєте на увазі, що у випадку> він відкриває файл і очищає його, а у випадку >> відкриває лише його"?
Razvan

2
так, але слід зазначити, що в цій ситуації >переспрямування відкриє файл та скоротить його перед запуском оболонки grep.
glenn jackman

1
Подивіться мою відповідь, якщо ви не хочете використовувати тимчасовий файл, але, будь ласка, не голосуйте за цей коментар.
Zack Morris

Замість цього слід прийняти відповідь за допомогою spongeкоманди .
vlz

96

Для цього використовуйте губку . Його частина є більше.

Спробуйте виконати цю команду:

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

4
Дякую за відповідь. Як корисне доповнення, якщо ви використовуєте доморощену мову на Mac, можете використовувати brew install moreutils.
Ентоні Паноццо

2
Або sudo apt-get install moreutilsна системах на базі Debian.
Йона

3
Блін! Дякую, що познайомили мене з moreutils =) там є кілька приємних програм!
мережі

велике спасибі, moreutils за порятунок! губка, як бос!
aququadro

3
Слово обережності, "губка" руйнує, тому, якщо у вашій команді є помилка, ви можете стерти свій вхідний файл (як це було при першій спробі губки). Переконайтеся, що ваша команда працює, та / або вхідний файл знаходиться під контролем версій, якщо ви намагаєтеся зробити ітерацію, щоб команда працювала.
user107172

18

Замість цього використовуйте sed:

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

1
iirc -i- це лише розширення GNU, лише зазначивши.
c00kiemon5ter

3
На * BSD (а отже, і на OSX) ви можете сказати, -i ''що розширення не є суворо обов'язковим, але -iопція вимагає певного аргументу.
триплі

14

спробуйте цей простий

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

Цього разу ваш файл не буде порожнім :), а ваш вихід також надрукується на вашому терміналі.


1
Мені подобається це рішення! І якщо ви не хочете, щоб його надрукували в терміналі, ви все одно можете перенаправити вихідні дані в /dev/nullподібні місця.
Фрозн

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для досягнення того ж (як вже було показано в інших відповідях), однак in-place ( -i) - це нестандартне розширення FreeBSD (може працювати по-різному між Unix / Linux), і в основному це s tream ed 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 спочатку виконує команду, а потім завантажує stdout catі ставить його як перший аргумент echo. Звичайно, непридатні для друку змінні не виводять належним чином і не пошкоджують дані. Не намагайтеся перенаправити файл назад до себе, це просто не може бути добре.
Лінч,

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 відкриває всі труби асинхронно, і ми повинні обійти це, використовуючи sleepтак YMMV.

У вашому прикладі:

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

Ви можете використовувати серп з POSIX Awk:

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

Приклад


1
Мабуть, слід зазначити, що "slurp" означає "зчитування всього файлу в пам'ять". Якщо у вас великий вхідний файл, можливо, ви хочете цього уникнути.
триплі

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 не працює належним чином, ви втратите вміст файлу. Це можна пом'якшити, відновивши файл, якщо 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

Коротке пояснення або навіть коментарі можуть бути корисними.
Багатий

я думаю, це працює, тому що екстраполяція рядків виконується перед оператором перенаправлення, але я не знаю точно
Виктор Пупкин

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"
}

-2

Я зазвичай використовую програму трійника для цього:

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

Він створює і видаляє тимчасовий файл сам по собі.


Вибачте, teeне гарантується робота. Див. Askubuntu.com/a/752451/335781 .
studgeek
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.