Jak mogę uzyskać pierwsze dopasowanie z rozwinięcia symboli wieloznacznych?

38

Powłoki, takie jak Bash i Zsh, zamieniają symbol wieloznaczny na argumenty, tyle argumentów ile odpowiada wzorowi:

$ echo *.txt
1.txt 2.txt 3.txt

Ale co jeśli chcę tylko zwrócić pierwszy mecz, a nie wszystkie mecze?

$ echo *.txt
1.txt

Nie mam nic przeciwko rozwiązaniom specyficznym dla powłoki, ale chciałbym rozwiązania, które działa z białymi spacjami w nazwach plików.

Flimm
źródło
ls * .txt | głowa -1?
Archemar
1
@Archemar: nie działa z nowymi liniami w nazwach plików.
Flimm,

Odpowiedzi:

25

Jednym z solidnych sposobów bash jest rozwinięcie do tablicy i wyprowadzenie tylko pierwszego elementu:

pattern="*.txt"
files=( $pattern )
echo "${files[0]}"  # printf is safer!

(Możesz nawet po prostu echo $files, brakujący indeks jest traktowany jako [0].)

To bezpiecznie obsługuje spację / tab / znak nowej linii i inne metaznaki podczas rozwijania nazw plików. Pamiętaj, że obowiązujące ustawienia regionalne mogą zmienić to, co jest „pierwsze”.

Możesz to również zrobić interaktywnie za pomocą funkcji uzupełniania bash :

_echo() {
    local cur=${COMP_WORDS[COMP_CWORD]}   # string to expand

    if compgen -G "$cur*" > /dev/null; then
        local files=( ${cur:+$cur*} )   # don't expand empty input as *
        [ ${#files} -ge 1 ] && COMPREPLY=( "${files[0]}" )
    fi
}
complete -o bashdefault -F _echo echo

Powoduje to powiązanie _echofunkcji z uzupełnieniem argumentów echopolecenia (zastępując normalne zakończenie). Dodatkowy „*” jest dołączony do powyższego kodu, możesz po prostu nacisnąć tabulację na częściowej nazwie pliku i mam nadzieję, że coś dobrego się wydarzy.

Kod jest nieco zawiły, zamiast ustawiania lub zakładania nullglob( shopt -s nullglob) sprawdzamy, czy compgen -Gmożna rozszerzyć glob do niektórych dopasowań, następnie bezpiecznie rozwijamy do tablicy, a na końcu ustawiamy COMPREPLY, aby cytowanie było solidne.

Możesz częściowo to zrobić (programowo rozwinąć glob) za pomocą bash compgen -G, ale nie jest to niezawodne, ponieważ wypisywane jest nie przywołane na standardowe wyjście.

Jak zwykle, uzupełnianie jest dość obciążone, co przerywa uzupełnianie innych rzeczy, w tym zmiennych środowiskowych (zobacz tutaj_bash_def_completion() funkcję , aby uzyskać szczegółowe informacje na temat emulacji domyślnego zachowania).

Możesz także użyć compgenpoza funkcją uzupełniania:

files=( $(compgen -W "$pattern") )

Należy zauważyć, że „~” nie jest globem, jest obsługiwane przez bash na osobnym etapie ekspansji, podobnie jak zmienne $ i inne rozszerzenia. compgen -Gpo prostu dokonuje globowania nazw plików, ale compgen -Wzapewnia wszystkie domyślne rozszerzenia bash, choć być może zbyt wiele rozszerzeń (w tym ``i $()). W przeciwieństwie do -G, -W jest bezpiecznie cytowany (nie umiem wyjaśnić różnicy). Ponieważ chodzi o -Wto, że rozwija on tokeny, oznacza to, że rozwinie „a” do „a”, nawet jeśli taki plik nie istnieje, więc być może nie jest idealny.

Jest to łatwiejsze do zrozumienia, ale może mieć niepożądane skutki uboczne:

_echo() {
    local cur=${COMP_WORDS[COMP_CWORD]}
    local files=( $(compgen -W "$cur") ) 
    printf -v COMPREPLY %q "${files[0]}"  
}

Następnie:

touch $'curious \n filename'

echo curious*tab

Zwróć uwagę na użycie, printf %qaby bezpiecznie podać wartości.

Ostatnią opcją jest użycie wyjścia rozdzielanego cyframi 0 w narzędziach GNU (zobacz bash FAQ ):

pattern="*.txt"
while IFS= read -r -d $'\0' filename; do 
    printf '%q' "$filename"; 
    break; 
done < <(find . -maxdepth 1 -name "$pattern" -printf "%f\0" | sort -z )

Ta opcja daje ci nieco większą kontrolę nad kolejnością sortowania (kolejność przy rozszerzaniu globu będzie zależała od twoich ustawień regionalnych / LC_COLLATEi może, ale nie musi składać), ale poza tym jest dość dużym młotem dla tak małego problemu ;-)

pan. spuratic
źródło
20

W Zsh użyj [1] kwalifikatora glob . Zauważ, że chociaż ten szczególny przypadek zwraca co najwyżej jedno dopasowanie, to wciąż jest to lista, a globusy nie są rozwijane w kontekstach, które oczekują pojedynczego słowa, takiego jak przypisania (oprócz przypisań tablicowych).

echo *.txt([1])

W ksh lub bash możesz upchnąć całą listę dopasowań w tablicy i użyć pierwszego elementu.

tmp=(*.txt)
echo "${tmp[0]}"

W dowolnej powłoce możesz ustawić parametry pozycyjne i użyć pierwszego.

set -- *.txt
echo "$1"

Spowoduje to zablokowanie parametrów pozycji. Jeśli tego nie chcesz, możesz użyć podpowłoki.

echo "$(set -- *.txt; echo "$1")"

Możesz także użyć funkcji, która ma swój własny zestaw parametrów pozycyjnych.

set_to_first () {
  eval "$1=\"\$2\""
}
set_to_first f *.txt
echo "$f"
Gilles „SO- przestań być zły”
źródło
1
Aby zdobyć pierwsze mecze $ n $, możesz użyć*.txt([1,n])
Emre
6

Próbować:

for i in *.txt; do printf '%s\n' "$i"; break; done
1.txt

Uwaga: rozszerzenie nazwy pliku jest sortowane zgodnie z kolejnością zestawiania obowiązującą w bieżących ustawieniach narodowych.

Cuonglm
źródło
3

Proste rozwiązanie:

sh -c 'echo "$1"' sh *.txt

Lub użyj, printfjeśli wolisz.

G-Man mówi „Przywróć Monikę”
źródło
1

Natknąłem się na to stare pytanie, zastanawiając się nad tym samym. Skończyło się na tym:

echo $(ls *.txt | head -n1)

Można, oczywiście, należy wymienić headz taili -n1z żadnym innym numerem.


Powyższe nie zadziała, jeśli pracujesz z plikami, które mają nazwy nowego wiersza. Aby pracować z nowymi liniami, możesz użyć dowolnego z tych:

  • ls -b *.txt | head -n1 | sed -E 's/\\n/\n/g' (Nie działa na BSD)
  • ls -b *.txt | head -n1 | sed -e 's/\\n/\'$'\n/g'
  • ls -b *.txt | head -n1 | perl -pe 's/\\n/\n/g'
  • echo -e "$(ls -b *.txt | head -n1)" (Działa z dowolnym znakiem specjalnym)
użytkownik149485
źródło
3
Nie, to się nie powiedzie, jeśli nazwa pliku ma nowe linie.
Izaak
7
w jakim szalonym świecie żyjemy, w których nazwy plików zawierają nowe linie?
billynoah
-1

Przypadkiem użycia, z którym często się spotykam, jest zdefiniowanie katalogu górnego / dolnego po rozszerzeniu globalnym (np. Katalog pełen wersjonowanych zestawów SDK lub narzędzi do budowania). W tej sytuacji zwykle chcę zapisać nazwę katalogu w zmiennej, aby użyć jej w kilku miejscach w skrypcie powłoki.

To polecenie zwykle robi to dla mnie:

export SDK_DIR=$(dirname /path/to/versioned/sdks/*/. | tail -n1)

Uwaga: Rozszerzenie Glob nie posortuje folderów według semver; Zostałeś ostrzeżony. Jest to świetne, jeśli masz Dockerfiletylko jedną wersję katalogu, ale ta wersja katalogów może różnić się w zależności od obrazu 🤷

Andrew Odri
źródło
Witamy w U&L! To obsługuje większość nazw katalogów, ale nie obsługuje nazw katalogów z nową linią. Spróbuj utworzyć taki katalog mkdir "$(echo one; echo two)"i zobacz, co mam na myśli.
Flimm
Jaka jest zaleta w porównaniu z innymi alternatywami, zwłaszcza wersją, która używa tail?
RalfFriedl
Standard dirnameprzyjmuje tylko jedną ścieżkę, więc nie można polegać na wielu ścieżkach, chyba że wiesz, że obsługuje ją Twoja konkretna implementacja.
Kusalananda
@Flimm Dobra uwaga; Myślę, że większość programistów miałaby większe problemy do rozwiązania, gdyby ich struktura folderów zawierała nowe wiersze ... Nigdy nie musiałem sobie z tym radzić i nie oczekuję, że użyję pół przyzwoitych kontenerów i oprogramowania, którego używam
Andrew Odri
@RalfFriedl Dobre pytanie; to zasadniczo odfiltrowuje wszystko, co nie jest prawidłowym katalogiem (i nie będzie wyświetlać / przechodzić. i ..)
Andrew Odri