Jak usunąć ostatni wiersz wszystkich plików z katalogu?

17

Mam wiele plików tekstowych w katalogu i chcę usunąć ostatnią linię każdego pliku w katalogu.

Jak mogę to zrobić?

ThehalfarisedPheonix
źródło
6
Czego próbowałeś? unix.stackexchange.com/help/how-to-ask : „Dzielenie się badaniami pomaga wszystkim. Powiedz nam, co znalazłeś i dlaczego nie spełniło twoich potrzeb. To pokazuje, że poświęciłeś czas, aby spróbować sobie pomóc , nie pozwala nam powtarzać oczywistych odpowiedzi, a przede wszystkim pomaga uzyskać bardziej konkretną i odpowiednią odpowiedź! ”
Patrick
Dlaczego myśl, że ktoś przypadkowo zastosuje to do / etc, ma wyjątkową jakość indukcji cringe :)
rackandboneman

Odpowiedzi:

4

Jeśli masz dostęp do vim, możesz użyć:

for file in ./*
do
  if [ -f "${file}" ]
  then
    vim -c '$d' -c "wq" "${file}"
  fi
done
R Sahu
źródło
16

Możesz użyć tego przyjemnego onelinera, jeśli masz GNU sed.

 sed -i '$ d' ./*

Spowoduje to usunięcie ostatniego wiersza każdego nie ukrytego pliku w bieżącym katalogu. Przełącznik -iGNU sedoznacza działanie w miejscu i '$ d'nakazuje sedusunięcie ostatniego wiersza ( $co oznacza ostatnie i doznacza usunięcie).

StefanR
źródło
3
Spowoduje to wyrzucenie błędów (i nic nie robienie), jeśli folder zawiera coś innego niż zwykły plik, np. Inny folder ...
Najib Idrissi
1
@StefanR Używasz -iGNUizmu, więc jest to dyskusja, ale straciłbym swoją starą brodę, gdybym nie zauważył, że niektóre starsze wersje sednie pozwalają na umieszczanie białych znaków pomiędzy $i d(lub, ogólnie między wzorcem a poleceniem).
zwolnienie
1
@zwol Jak napisałem, spowoduje to błąd , a nie ostrzeżenie, a sed zrezygnuje, gdy osiągnie ten plik (przynajmniej z wersją sed, którą mam). Następne pliki nie będą przetwarzane. Wyrzucanie komunikatów o błędach byłoby strasznym pomysłem, ponieważ nawet nie wiedziałeś, że to się stało! Z zsh możesz użyć *(.)do globowania dla zwykłych plików, nie wiem o innych powłokach.
Najib Idrissi
@NajibIdrissi Hmm, masz rację. To mnie zaskakuje; Spodziewałbym się, że to narzeka na katalog, ale potem przejdę do następnego pliku w wierszu poleceń. Właściwie myślę, że zgłoszę to jako błąd.
zwolnienie
@don_crissti Mam też GNU sed v4.3 ... Nie wiem co ci powiedzieć, właśnie przetestowałem. gist.github.com/nidrissi/66fad6be334234f5dbb41c539d84d61e
Najib Idrissi
11

Wszystkie pozostałe odpowiedzi mają problemy, jeśli katalog zawiera coś innego niż zwykły plik lub plik ze spacjami / znakami nowej nazwy. Oto coś, co działa niezależnie:

find "$dir" -type f -exec sed -i '$d' '{}' '+'
  • find "$dir" -type f: znajdź pliki w katalogu $dir
    • -type f które są zwykłymi plikami;
    • -exec wykonaj polecenie dla każdego znalezionego pliku
    • sed -i: edytuj pliki na miejscu;
    • '$d': usuń ( d) ostatnią ( $) linię.
    • '+': każe sedfindowi dodawać argumenty do (nieco bardziej wydajne niż uruchamianie polecenia dla każdego pliku osobno, dzięki @zwol).

Jeśli nie chcesz schodzić do podkatalogów, możesz dodać argument -maxdepth 1do find.

Najib Idrissi
źródło
1
Hmm, ale w przeciwieństwie do innych odpowiedzi, to schodzi do podkatalogów. (Również przy obecnych wersjach findjest to bardziej efektywnie napisane find $dir -type f -exec sed -i '$d' '{}' '+'.)
zwolnij
@zwol Dzięki, dodałem to do odpowiedzi.
Najib Idrissi
-print0nie występuje w pełnym poleceniu, po co to wyjaśniać?
Ruslan
1
Ponadto -depth 0nie działa (findutils 4.4.2), powinno być -maxdepth 1.
Ruslan
@ Ruslan Miałem pierwszą wersję, w której używałem xargs, ale potem zapamiętałem -exec.
Najib Idrissi
9

Korzystanie z GNU sed -i '$d'oznacza odczytanie pełnego pliku i zrobienie jego kopii bez ostatniego wiersza, podczas gdy o wiele bardziej efektywne byłoby po prostu obcięcie pliku na miejscu (przynajmniej w przypadku dużych plików).

Dzięki GNU truncatemożesz:

for file in ./*; do
  [ -f "$file" ] &&
    length=$(tail -n 1 "$file" | wc -c) &&
    [ "$length" -gt 0 ] &&
    truncate -s "-$length" "$file"
done

Jeśli pliki są stosunkowo małe, prawdopodobnie byłoby to mniej wydajne, ponieważ uruchamia kilka poleceń na plik.

Zauważ, że w przypadku plików, które zawierają dodatkowe bajty po ostatnim znaku nowej linii (po ostatnim wierszu) lub innymi słowy, jeśli mają nieograniczoną linię ostatniego wiersza , wówczas w zależności od tailimplementacji tail -n 1zwrócą tylko te dodatkowe bajty (jak GNU tail), lub ostatnia (właściwie rozdzielona) linia i te dodatkowe bajty.

Stéphane Chazelas
źródło
potrzebujesz |wc -cw tailrozmowie? (lub a ${#length})
Jeff Schaller
@JeffSchaller. Ups wc -c rzeczywiście było zamierzone. ${#length}nie działałby, ponieważ zlicza znaki, a nie bajty i $(...)usuwałby znak nowego wiersza, więc ${#...}byłby wyłączony o jeden, nawet gdyby wszystkie znaki były jednobajtowe.
Stéphane Chazelas,
6

Bardziej przenośne podejście:

for f in ./*
do
test -f "$f" && ed -s "$f" <<\IN
d
w
q
IN
done

Nie sądzę, żeby to wymagało wyjaśnienia ... z wyjątkiem tego, że w tym przypadku djest to samo, $dponieważ eddomyślnie wybiera ostatnią linię.
To nie będzie wyszukiwać rekurencyjnie i nie będzie przetwarzać ukrytych plików (inaczej dotfiles).
Jeśli chcesz je edytować, zobacz Jak dopasować * do ukrytych plików w katalogu

don_crissti
źródło
Ładny! Jeśli zmienisz [[]]na, []to będzie on w pełni zgodny z POSIX. ( [[ ... ]]to bashism.)
Wildcard
@Wildcard - dzięki, zmieniono (choć [[to nie jest bashism )
don_crissti
Powinienem powiedzieć, że nie jest to POSIX. :)
Wildcard
3

Jednowarstwowy zgodny z POSIX dla wszystkich plików rozpoczynających się rekurencyjnie w bieżącym katalogu, w tym plików kropkowych:

find . -type f -exec sh -c 'for f; do printf "\$d\nx\n" | ex "$f"; done' sh {} +

Tylko dla .txtplików, nie rekurencyjnie:

find . -path '*/*/*' -prune -o -type f -name '*.txt' -exec sh -c 'for f; do printf "\$d\nx\n" | ex "$f"; done' sh {} +

Zobacz także:

Dzika karta
źródło