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.