Bagaimana saya bisa mendapatkan pertandingan pertama dari ekspansi wildcard?

39

Kerang seperti Bash dan Zsh memperluas wildcard menjadi argumen, sebanyak mungkin argumen yang cocok dengan polanya:

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

Tetapi bagaimana jika saya hanya ingin pertandingan pertama dikembalikan, tidak semua pertandingan?

$ echo *.txt
1.txt

Saya tidak keberatan dengan solusi khusus shell, tetapi saya ingin solusi yang bekerja dengan spasi putih dalam nama file.

Flimm
sumber
ls * .txt | kepala -1?
Archemar
1
@Archemar: tidak bekerja dengan baris baru dalam nama file.
Flimm

Jawaban:

26

Salah satu cara yang kuat dalam bash adalah memperluas ke array, dan output elemen pertama saja:

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

(Anda bahkan bisa saja echo $files, indeks yang hilang diperlakukan sebagai [0].)

Ini dengan aman menangani spasi / tab / baris baru dan karakter metakarakter lainnya saat memperluas nama file. Perhatikan bahwa pengaturan lokal yang berlaku dapat mengubah apa yang "pertama" itu.

Anda juga dapat melakukan ini secara interaktif dengan fungsi penyelesaian 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

Ini mengikat _echofungsi untuk melengkapi argumen ke echoperintah (menimpa penyelesaian normal). "*" Tambahan ditambahkan dalam kode di atas, Anda bisa menekan tab pada nama file parsial dan mudah-mudahan hal yang benar akan terjadi.

Kode sedikit berbelit-belit, daripada disetel atau diasumsikan nullglob( shopt -s nullglob) kita periksa compgen -Gdapat memperluas gumpalan ke beberapa kecocokan, kemudian kita memperluas dengan aman ke dalam array, dan akhirnya mengatur KOMPREPLI sehingga kuotasi kuat.

Anda bisa melakukan ini sebagian (secara terprogram memperluas gumpalan) dengan bash compgen -G, tetapi tidak kuat karena outputnya tidak dikutip ke stdout.

Seperti biasa, penyelesaian agak penuh, hal ini merusak penyelesaian hal-hal lain, termasuk variabel lingkungan (lihat _bash_def_completion()fungsi di sini untuk detail meniru perilaku default).

Anda juga bisa menggunakan compgen luar fungsi penyelesaian:

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

Satu hal yang perlu diperhatikan adalah bahwa "~" bukan glob, itu ditangani oleh bash dalam tahap ekspansi yang terpisah, seperti $ variable dan ekspansi lainnya. compgen -Ghanya melakukan globbing nama file, tetapi compgen -Wmemberi Anda semua perluasan default bash, meskipun mungkin terlalu banyak ekspansi (termasuk ``dan $()). Tidak seperti -Gitu-W adalah aman dikutip (saya tidak bisa menjelaskan perbedaan tersebut). Karena tujuannya -Wadalah untuk memperluas token, ini berarti ia akan memperluas "a" ke "a" bahkan jika tidak ada file seperti itu, jadi itu mungkin tidak ideal.

Ini lebih mudah dipahami, tetapi mungkin memiliki efek samping yang tidak diinginkan:

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

Kemudian:

touch $'curious \n filename'

echo curious*tab

Perhatikan penggunaan printf %quntuk mengutip nilai-nilai secara aman.

Salah satu opsi terakhir adalah menggunakan output yang dibatasi 0 dengan utilitas GNU (lihat 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 )

Opsi ini memberi Anda sedikit lebih banyak kontrol atas urutan penyortiran (urutan ketika memperluas gumpalan akan tunduk pada lokal Anda / LC_COLLATEdan mungkin atau mungkin tidak melipat kasing), tetapi sebaliknya palu yang agak besar untuk masalah kecil ;-)

mr.spuratic
sumber
21

Di zsh, gunakan [1] kualifikasi glob . Perhatikan bahwa meskipun case khusus ini mengembalikan paling banyak satu pertandingan, itu masih daftar, dan gumpalan tidak diperluas dalam konteks yang mengharapkan satu kata seperti tugas (penugasan array array).

echo *.txt([1])

Dalam ksh atau bash, Anda dapat memasukkan seluruh daftar kecocokan dalam sebuah array dan menggunakan elemen pertama.

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

Dalam shell apa pun, Anda dapat mengatur parameter posisi dan menggunakan yang pertama.

set -- *.txt
echo "$1"

Ini mengacaukan parameter posisi. Jika Anda tidak menginginkannya, Anda dapat menggunakan subkulit.

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

Anda juga dapat menggunakan fungsi, yang memiliki set parameter posisinya sendiri.

set_to_first () {
  eval "$1=\"\$2\""
}
set_to_first f *.txt
echo "$f"
Gilles 'SANGAT berhenti menjadi jahat'
sumber
1
Dan untuk mendapatkan $ n $ pertandingan pertama, Anda dapat menggunakan*.txt([1,n])
Emre
6

Mencoba:

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

Catatan bahwa ekspansi nama file diurutkan sesuai dengan urutan pengumpatan yang berlaku di lokal saat ini.

cuonglm
sumber
3

Solusi sederhana:

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

Atau gunakan printfjika Anda suka.

G-Man Mengatakan 'Reinstate Monica'
sumber
1

Saya hanya menemukan pertanyaan lama ini sambil bertanya-tanya hal yang sama. Saya berakhir dengan ini:

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

Anda dapat, tentu saja, ganti headdengan taildan -n1dengan nomor lainnya.


Di atas tidak akan berfungsi jika Anda bekerja dengan file yang memiliki baris baru dalam namanya. Untuk bekerja dengan baris baru, Anda dapat menggunakan salah satu dari ini:

  • ls -b *.txt | head -n1 | sed -E 's/\\n/\n/g' (Tidak berfungsi pada 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)" (Bekerja dengan karakter khusus apa saja)
pengguna149485
sumber
3
Tidak, itu akan gagal jika nama file memiliki baris baru.
Isaac
7
dunia seperti apa kita hidup di mana nama file berisi baris baru?
billynoah
-1

Kasus penggunaan yang sering saya temui adalah mendefinisikan direktori atas / bawah setelah ekspansi glob (mis. Direktori yang penuh dengan SDK versi atau alat bantu bangunan). Dalam situasi ini saya biasanya ingin menyimpan nama direktori itu ke variabel untuk digunakan di beberapa tempat dalam skrip shell.

Perintah ini biasanya melakukannya untuk saya:

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

Penafian: Perluasan global tidak akan menyortir folder Anda dengan semver; Anda sudah diperingatkan. Ini bagus jika Anda memiliki Dockerfileyang hanya satu versi direktori, tetapi versi direktori dapat bervariasi dari gambar ke gambar 🤷

Andrew Odri
sumber
Selamat datang di U&L! Itu menangani sebagian besar nama direktori, tetapi tidak menangani nama direktori dengan baris baru. Coba buat direktori seperti ini mkdir "$(echo one; echo two)"dan lihat apa yang saya maksud.
Flimm
Apa kelebihannya dibandingkan dengan alternatif lain, terutama versi yang digunakan tail?
RalfFriedl
Standar dirnamehanya membutuhkan satu nama path, jadi Anda tidak bisa mengandalkan itu bekerja pada beberapa nama path kecuali Anda tahu implementasi khusus Anda mendukungnya.
Kusalananda
@ Flimm Poin bagus; Saya pikir sebagian besar pengembang akan memiliki masalah yang lebih besar untuk dihadapi jika struktur folder mereka memiliki baris baru di dalamnya ... Saya tidak pernah harus berurusan dengan itu, dan jangan berharap dengan wadah dan perangkat lunak setengah layak yang saya gunakan
Andrew Odri
@RalfFriedl Pertanyaan bagus; ini pada dasarnya menyaring apa pun yang bukan direktori yang valid (dan tidak akan mencantumkan / melintasi. dan ..)
Andrew Odri