Jak mogę użyć pliku w poleceniu i przekierować dane wyjściowe do tego samego pliku bez obcinania go?


98

Zasadniczo chcę wziąć tekst wejściowy z pliku, usunąć wiersz z tego pliku i wysłać dane wyjściowe z powrotem do tego samego pliku. Coś w tym kierunku, jeśli to uczyni to jaśniejszym.

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

jednak kiedy to robię, otrzymuję pusty plik. jakieś pomysły?


Odpowiedzi:


85

Nie możesz tego zrobić, ponieważ bash najpierw przetwarza przekierowania, a następnie wykonuje polecenie. Więc zanim grep spojrzy na nazwa_pliku, jest już pusty. Możesz jednak użyć pliku tymczasowego.

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

w ten sposób rozważ użycie mktempdo utworzenia pliku tmp, ale pamiętaj, że nie jest to POSIX.


47
Powód, dla którego nie możesz tego zrobić: bash najpierw przetwarza przekierowania, a następnie wykonuje polecenie. Więc zanim grep spojrzy na nazwa_pliku, jest już pusty.
— glenn jackman

1
@glennjackman: przez "przekierowanie procesów masz na myśli, że w przypadku> otwiera plik i czyści go, aw przypadku >> tylko go otwiera"?
— Razvan,

2
tak, ale uwaga w tej sytuacji, >przekierowanie otworzy plik i obetnie go przed uruchomieniem powłoki grep.
— glenn jackman,

1
Zobacz moją odpowiedź, jeśli nie chcesz używać pliku tymczasowego, ale nie głosuj za tym komentarzem.
— Zack Morris

Zamiast tego należy zaakceptować odpowiedź za pomocą spongepolecenia .
— vlz

98

Do tego typu zadań używaj gąbki . Jest częścią moreutils.

Wypróbuj to polecenie:

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

4
Dziękuję za odpowiedź. Jako potencjalnie pomocny dodatek, jeśli używasz homebrew na komputerze Mac, możesz użyć brew install moreutils.
— Anthony Panozzo

2
Lub sudo apt-get install moreutilsw systemach opartych na Debianie.
— Jonah

3
Cholera! Dziękuję za wprowadzenie do moreutils =) kilku fajnych programów!
— netigger

dziękuję bardzo, więcej narzędzi na ratunek! gąbka jak szef!
— aqquadro

3
Słowo ostrzeżenia, "sponge" jest destrukcyjne, więc jeśli masz błąd w swoim poleceniu, możesz wyczyścić plik wejściowy (tak jak zrobiłem to po raz pierwszy próbując sponge). Upewnij się, że polecenie działa i / lub plik wejściowy jest pod kontrolą wersji, jeśli próbujesz wykonać iterację, aby polecenie działało.
— user107172

19

Zamiast tego użyj seda:

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

1
iirc -ijest tylko rozszerzeniem GNU, wystarczy zauważyć.
— c00kiemon5ter

4
Na * BSD (a więc i OSX) Można powiedzieć -i ''więc rozszerzenie nie jest to bezwzględnie obowiązkowe, ale -iopcja wymaga jakiś argument.
— tripleee

16

spróbuj tego prostego

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

Twój plik nie będzie tym razem pusty :), a wynik jest również drukowany na terminalu.


1
Podoba mi się to rozwiązanie! A jeśli nie chcesz, aby był on drukowany w terminalu, nadal możesz przekierować wyjście do /dev/nulllub podobnych miejsc.
— Frozn

4
Spowoduje to również wyczyszczenie zawartości pliku. Czy jest to spowodowane różnicą GNU / BSD? Jestem na macOS ...
— ssc

7

Nie możesz użyć operatora przekierowania ( >lub >>) do tego samego pliku, ponieważ ma on wyższy priorytet i utworzy / obetnie plik przed wywołaniem polecenia. Aby tego uniknąć, należy użyć odpowiednich narzędzi, takich jak tee, sponge, sed -ilub jakiekolwiek inne narzędzie, które może zapisywać wyniki do pliku (na przykład sort file -o file).

Zasadniczo przekierowywanie wejścia do tego samego oryginalnego pliku nie ma sensu i powinieneś do tego użyć odpowiednich edytorów lokalnych, na przykład edytora Ex (część Vima):

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

gdzie:

  • '+cmd'/ -c- uruchom dowolną komendę Ex / Vim
  • g/pattern/d- usuń linie pasujące do wzorca za pomocą funkcji global ( help :g)
  • -s- tryb cichy ( man ex)
  • -c wq- wykonaj :writei :quitpolecenia

Można użyć sed, aby osiągnąć ten sam (jak już pokazano w innych odpowiedzi), jednak w miejscu ( -i) jest niestandardowym rozszerzeniem FreeBSD (może działać w różny sposób między Unix / Linux) iw zasadzie jest to s TREAM ed itor, a nie edytor plików . Zobacz: Czy tryb Ex ma jakieś praktyczne zastosowanie?


6

Alternatywa dla jednego linera - ustaw zawartość pliku jako zmienną:

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

4

Ponieważ to pytanie jest najlepszym wynikiem w wyszukiwarkach, oto jedna linijka oparta na https://serverfault.com/a/547331, która używa podpowłoki zamiast sponge(która często nie jest częścią instalacji waniliowej, takiej jak OS X) :

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

Ogólny przypadek to:

echo "$(cat file_name)" > file_name

Edytuj, powyższe rozwiązanie ma pewne zastrzeżenia:

  • printf '%s' <string>powinno być używane zamiast echo <string>, aby pliki zawierające -nnie powodowały niepożądanego zachowania.
  • Zastępowanie poleceń usuwa końcowe znaki nowej linii ( jest to błąd / funkcja powłok takich jak bash ), więc powinniśmy dodać znak postfiksowy, taki jak xdo wyjścia i usunąć go na zewnątrz poprzez rozwinięcie parametrów zmiennej tymczasowej, takiej jak${v%x} .
  • Użycie zmiennej tymczasowej $vpodbija wartość dowolnej istniejącej zmiennej $vw bieżącym środowisku powłoki, więc powinniśmy zagnieździć całe wyrażenie w nawiasach, aby zachować poprzednią wartość.
  • Innym błędem / cechą powłok, takich jak bash, jest to, że podstawianie poleceń usuwa niedrukowalne znaki, takie jak nullz wyniku. Sprawdziłem to, dzwoniąc dd if=/dev/zero bs=1 count=1 >> file_namei przeglądając szesnastkowo z cat file_name | xxd -p. Ale echo $(cat file_name) | xxd -pjest rozebrany. Tak więc , jak zauważył Lynch, nie należy używać tej odpowiedzi na plikach binarnych ani na niczym nie używającym znaków niedrukowalnych .

Ogólne rozwiązanie (choć nieco wolniejsze, bardziej obciążające pamięć i nadal usuwające niedrukowalne znaki) to:

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

Test z 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

Powinien wydrukować:

hello
world

Natomiast wywołanie cat file_uniquely_named.txt > file_uniquely_named.txtaktualnej powłoki:

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

Drukuje pusty ciąg.

Nie testowałem tego na dużych plikach (prawdopodobnie powyżej 2 lub 4 GB).

Zapożyczyłem tę odpowiedź od Hart Simha i kos .


2
Oczywiście nie będzie działać z dużym plikiem. To nie może być dobre rozwiązanie lub działać przez cały czas. To, co się dzieje, polega na tym, że bash najpierw wykonuje polecenie, a następnie ładuje standardowe wyjście cati umieszcza je jako pierwszy argument echo. Oczywiście zmienne niedrukowalne nie będą poprawnie wyprowadzane i uszkadzają dane. Nie próbuj przekierowywać pliku z powrotem do siebie, po prostu nie może być dobry.
— Lynch,

1

Istnieje również ed(jako alternatywa dla 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

Możesz to zrobić za pomocą podstawiania procesów .

To trochę hack, ponieważ bash otwiera wszystkie potoki asynchronicznie i musimy to obejść, używając sleepYMMV.

W twoim przykładzie:

grep -v 'seg[0-9]\{1,\}\.[0-9]\{1\}' file_name > >(sleep 1 && cat > file_name)
  • >(sleep 1 && cat > file_name) tworzy plik tymczasowy, który otrzymuje dane wyjściowe od grep
  • sleep 1 opóźnienia na sekundę, aby dać grepowi czas na przeanalizowanie pliku wejściowego
  • w końcu cat > file_namezapisuje dane wyjściowe

1

Możesz używać slurp z POSIX Awk:

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

Przykład


1
Być może należy zauważyć, że „slurp” oznacza „wczytaj cały plik do pamięci”. Jeśli masz duży plik wejściowy, być może chcesz tego uniknąć.
— tripleee

1

Jest to bardzo możliwe, musisz tylko upewnić się, że zanim napiszesz wynik, zapisujesz go w innym pliku. Można to zrobić, usuwając plik po otwarciu do niego deskryptora pliku, ale przed zapisaniem do niego:

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

Lub wiersz po wierszu, aby lepiej to zrozumieć:

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

Jest to nadal ryzykowne, ponieważ jeśli polecenie COMMAND nie zadziała poprawnie, utracisz zawartość pliku. Można to złagodzić, przywracając plik, jeśli COMMAND zwraca niezerowy kod zakończenia:

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

Możemy również zdefiniować funkcję powłoki, aby ułatwić korzystanie z:

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

Przykład:

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

Pamiętaj również, że zachowa to pełną kopię oryginalnego pliku (do czasu zamknięcia trzeciego deskryptora pliku). Jeśli używasz Linuksa, a plik, na którym przetwarzasz, jest zbyt duży, aby zmieścić się dwukrotnie na dysku, możesz sprawdzić ten skrypt , który przekieruje plik do określonego polecenia blok po bloku, jednocześnie usuwając przydział już przetworzonego Bloki. Jak zawsze przeczytaj ostrzeżenia na stronie użytkowania.


0

Spróbuj tego

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

cat testfile
AAA
BBB
CCC

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

Pomocne może być krótkie wyjaśnienie lub nawet komentarz.
— Rich

myślę, że to działa, ponieważ ekstrapolacja ciągów jest wykonywana przed operatorem przekierowania, ale nie wiem dokładnie
— Виктор Пупкин

0

Poniższe osiągną to samo, co spongerobi, bez konieczności moreutils:

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

Te --random-source=/dev/zerosztuczki partshuf język robi jego rzecz bez jakiegokolwiek szuranie w ogóle, więc będzie buforować swój wkład bez zmieniania go.

Jednak prawdą jest, że użycie pliku tymczasowego jest najlepsze ze względu na wydajność. Oto funkcja, którą napisałem, która zrobi to za Ciebie w uogólniony sposób:

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

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.