Jak zdobyć ostatnie N plików w katalogu?

15

Mam wiele plików uporządkowanych według nazw plików w katalogu. Chcę skopiować ostatnie N (powiedzmy N = 4) plików do mojego katalogu domowego. Jak mam to zrobić?

cp ./<the final 4 files> ~/

Hazard Sibbs
źródło
1
W przypadku wszystkich poniższych odpowiedzi może być konieczne dodanie „LC_ALL = ...” przed poleceniami korzystającymi z zakresów, aby ich zakresy używały odpowiednich ustawień regionalnych (ustawienia narodowe mogą się zmieniać, a następnie kolejność znaków może się zmieniać np. w niektórych lokalizacjach [az] może zawierać wszystkie litery alfabetu oprócz „Z”, ponieważ litery są uporządkowane: aAbBcC .... zZ. Istnieją inne warianty (litery akcentowane i jeszcze bardziej egzotyczne uporządkowanie Zwykłym wyborem jest:LC_ALL=C
Olivier Dulac

Odpowiedzi:

23

Można to łatwo zrobić za pomocą tablic bash / ksh93 / zsh:

a=(*)
cp -- "${a[@]: -4}" ~/

Działa to w przypadku wszystkich nie ukrytych nazw plików, nawet jeśli zawierają spacje, tabulatory, znaki nowej linii lub inne trudne znaki (zakładając, że w bieżącym katalogu są co najmniej 4 pliki nie ukryte bash).

Jak to działa

  • a=(*)

    Spowoduje to utworzenie tablicy aze wszystkimi nazwami plików. Nazwy plików zwrócone przez bash są posortowane alfabetycznie. (Zakładam, że to właśnie rozumiesz przez „uporządkowane według nazwy pliku” ).

  • ${a[@]: -4}

    Zwraca ostatnie cztery elementy tablicy a(pod warunkiem, że tablica zawiera co najmniej 4 elementy z bash).

  • cp -- "${a[@]: -4}" ~/

    Spowoduje to skopiowanie czterech ostatnich nazw plików do katalogu domowego.

Aby skopiować i zmienić nazwę jednocześnie

Spowoduje to skopiowanie tylko czterech ostatnich plików do katalogu domowego, a jednocześnie dopisanie łańcucha a_do nazwy skopiowanego pliku:

a=(*)
for fname in "${a[@]: -4}"; do cp -- "$fname" ~/a_"$fname"; done

Skopiuj z innego katalogu, a także zmień nazwę

Jeśli użyjemy a=(./some_dir/*)zamiast a=(*), mamy problem z dołączeniem katalogu do nazwy pliku. Jednym z rozwiązań jest:

a=(./some_dir/*) 
for f in "${a[@]: -4}"; do cp "$f"  ~/a_"${f##*/}"; done

Innym rozwiązaniem jest użycie podpowłoki i cdkatalogu w podpowłoce:

(cd ./some_dir && a=(*) && for f in "${a[@]: -4}"; do cp -- "$f" ~/a_"$f"; done) 

Po zakończeniu podpowłoki powłoka wraca do oryginalnego katalogu.

Upewnienie się, że zamówienie jest spójne

Pytanie dotyczy plików „uporządkowanych według nazwy pliku”. To zamówienie, zauważa Olivier Dulac w komentarzach, będzie się różnić w zależności od regionu. Jeśli ważne jest, aby ustalone wyniki były niezależne od ustawień komputera, najlepiej jest wyraźnie określić ustawienia narodowe, gdy tablica ajest zdefiniowana. Na przykład:

LC_ALL=C a=(*)

Możesz dowiedzieć się, w którym aktualnie się znajdujesz, uruchamiając localepolecenie.

John1024
źródło
9

Jeśli używasz zsh, możesz zawrzeć w nawiasie ()listę tak zwanych globalnych kwalifikatorów, które wybierają pożądane pliki. W twoim przypadku tak byłoby

cp *(On[1,4]) ~/

Tutaj Onsortuje nazwy plików alfabetycznie w odwrotnej kolejności i [1,4]zajmuje tylko pierwsze 4 z nich.

Można zrobić to bardziej wytrzymałe wybierając tylko zwykłe pliki (z wyłączeniem katalogów, itp) z rur ., a także poprzez dołączenie --do cpkomendy w celu traktują pliki, których nazwy rozpoczynają się -prawidłowo, więc:

cp -- *(.On[1,4]) ~

Dodaj Dkwalifikator, jeśli chcesz również uwzględnić ukryte pliki (pliki kropek):

cp -- *(D.On[1,4]) ~
jimmij
źródło
2
Lub: *([-4,-1]).
Stéphane Chazelas,
2
@ StéphaneChazelas tak, pozwala wyjaśnić przyszłym czytelnikom, że sortowanie alfabetyczne w normalnym kierunku ( on) jest zachowaniem domyślnym, więc nie trzeba tego określać, a -przed liczbami nazwy plików liczą od dołu.
jimmij
7

Oto rozwiązanie wykorzystujące niezwykle proste polecenia bash.

find . -maxdepth 1 -type f | sort | tail -n 4 | while read -r file; do cp "$file" ~/; done

Wyjaśnienie:

find . -maxdepth 1 -type f

Znajduje wszystkie pliki w bieżącym katalogu.

sort

Sortuje alfabetycznie.

tail -n 4

Pokaż tylko 4 ostatnie wiersze.

while read -r file; do cp "$file" ~/; done

Pętle w każdej linii wykonują polecenie kopiowania.

gswong
źródło
6

Dopóki uznasz, że sortowanie powłoki jest przyjemne, możesz po prostu:

set -- /path/to/source/dir/*
[ "$#" -le 4 ] || shift "$(($#-4))"
cp "$@" /path/to/target/dir

Jest to bardzo podobne do bashoferowanego rozwiązania macierzy -specyficznego, ale powinno być przenośne dla dowolnej powłoki kompatybilnej z POSIX. Kilka uwag na temat obu metod:

  1. Ważne jest, abyś poprowadził swoje cpargumenty w / cp --lub dostał jedną z .kropek lub /na początku każdego imienia. Jeśli tego nie zrobisz, ryzykujesz pierwszym -myślnikiem cppierwszego argumentu, który może być interpretowany jako opcja i powodować niepowodzenie operacji lub w inny sposób generować niezamierzone wyniki.

    • Nawet jeśli pracujesz z bieżącym katalogiem jako katalogiem źródłowym, łatwo to zrobić tak jak ... set -- ./*lub array=(./*).
  2. Ważne jest, aby przy użyciu obu metod upewnić się, że masz przynajmniej tyle elementów w tablicy arg, ile próbujesz usunąć - robię to tutaj z rozszerzeniem matematycznym. shiftTylko się dzieje, jeśli istnieją co najmniej 4 pozycji w tablicy arg - i to tylko te pierwsze zmiany oddalony args które sprawiają, że nadwyżkę 4 ..

    • Więc w set 1 2 3 4 5przypadku 1odsunie to, ale w set 1 2 3przypadku niczego nie zmieni.
    • Na przykład: a=(1 2 3); echo "${a[@]: -4}"drukuje pustą linię.

Jeśli kopiujesz z jednego katalogu do drugiego, możesz użyć pax:

set -- /path/to/source/dir/*
[ "$#" -le 4 ] || shift "$(($#-4))"
pax -rws '|.*/|new_prefix|' "$@" /path/to/target/dir

... który zastosowałby sedpodstawienie w stylu do wszystkich nazw plików podczas ich kopiowania.

mikeserv
źródło
4

Jeśli są tylko pliki, a ich nazwy nie zawierają białych znaków ani znaków nowej linii (a nie zmodyfikowałeś $IFS) ani znaków globalnych (lub niektórych znaków niedrukowalnych z pewnymi implementacjami ls) i nie zaczynaj od ., możesz to zrobić :

cp -- $(ls | tail -n 4) ~/
Hauke ​​Laging
źródło
Nazywa się to zastępowaniem poleceń i jest naprawdę przydatne. tldp.org/LDP/abs/html/commandsub.html#CSPARENS
iyrin
Dzięki! To działa. Czy istnieje możliwość szybkiego dodania prefiksu a_do WSZYSTKICH czterech plików? Próbowałem echo "a_$(ls | tail -n 4)", ale poprzedza tylko pierwszy plik.
Sibbs Gambling
@SibbsGambling Moje podejście nie jest do tego odpowiednie, ale odpowiedź John1024 można łatwo dostosować do tego.
Hauke ​​Laging
5
Ogólnie parsowanie lsdanych wyjściowych jest uważane za złą formę, ponieważ zwykle strasznie zawodzi w nazwach plików zawierających nie tylko spacje, ale także tabulatory, znaki nowej linii lub inne prawidłowe, ale „trudne” znaki.
HBruijn
2
@HaukeLaging Nawet jeśli obecnie nie stanowi to problemu (ponieważ nie mam „skomplikowanych” nazw ogniowych w moim katalogu), mogę to zrobić za 2 lata i nagle coś mi spada na nogi ...
glglgl
1

To proste rozwiązanie bash bez kodu pętli. Skopiuj ostatnie 4 pliki z bieżącego katalogu do miejsca docelowego:

$ find . -maxdepth 1 -type f |tail -n -4|xargs cp -t "$destdir"
yasin_alm
źródło
1
Jak się upewnić, że finddaje to pliki w pożądanej kolejności? Jak upewnić się, że pliki zawierające znaki białych znaków (w tym znaki nowej linii) w ich nazwach są obsługiwane poprawnie?
Kusalananda