Apakah ada perintah bash yang menghitung file?

182

Apakah ada perintah bash yang menghitung jumlah file yang cocok dengan suatu pola?

Sebagai contoh, saya ingin mendapatkan jumlah semua file dalam direktori yang cocok dengan pola ini: log*

hudi
sumber

Jawaban:

243

Satu kalimat sederhana ini harus bekerja di shell apa pun, bukan hanya bash:

ls -1q log* | wc -l

ls -1q akan memberi Anda satu baris per file, meskipun mengandung spasi atau karakter khusus seperti baris baru.

Outputnya disalurkan ke wc -l, yang menghitung jumlah baris.

Daniel
sumber
10
Saya tidak akan menggunakan -l, karena itu memerlukan stat(2)pada setiap file dan untuk tujuan penghitungan menambahkan apa-apa.
camh
12
Saya tidak akan menggunakan ls, karena itu menciptakan proses anak. log*diperluas oleh shell, tidak ls, jadi sederhana echoakan dilakukan.
cdarke
2
Kecuali gema tidak akan berfungsi jika Anda memiliki nama file dengan spasi atau karakter khusus.
Daniel
4
@ WalterTross Itu benar (bukan berarti efisiensi adalah persyaratan dari pertanyaan awal). Saya juga baru saja menemukan bahwa -q menangani file dengan baris baru, bahkan ketika output bukan terminal. Dan bendera ini didukung oleh semua platform dan cangkang yang telah saya uji. Memperbarui jawabannya, terima kasih dan camh atas masukannya!
Daniel
3
Jika ada direktori yang disebut logsdalam direktori yang dimaksud, maka isi dari direktori log tersebut akan dihitung juga. Ini mungkin tidak disengaja.
mogsie
54

Anda dapat melakukan ini dengan aman (mis. Tidak akan disadap oleh file dengan spasi atau \nnamanya) dengan bash:

$ shopt -s nullglob
$ logfiles=(*.log)
$ echo ${#logfiles[@]}

Anda perlu mengaktifkan nullglobsehingga Anda tidak mendapatkan literal *.logdalam $logfiles array jika tidak ada file yang cocok. (Lihat Bagaimana "membatalkan" 'set -x'? Untuk contoh cara meresetnya dengan aman.)

Tikar
sumber
2
Mungkin secara eksplisit menunjukkan bahwa ini adalah jawaban Bash saja , terutama untuk pengunjung baru yang belum sepenuhnya mempercepat Perbedaan antara sh dan bash
tripleee
Juga, final shopt -u nullglobharus dilewati jika nullglobtidak disetel maka Anda mulai.
tripleee
Catatan: Mengganti *.logdengan hanya *akan menghitung direktori. Jika file yang ingin Anda hitung memiliki konvensi penamaan tradisional name.extension, gunakan *.*.
AlainD
52

Banyak jawaban di sini, tetapi beberapa tidak memperhitungkan

  • nama file dengan spasi, baris baru, atau karakter kontrol di dalamnya
  • nama file yang dimulai dengan tanda hubung (bayangkan file bernama -l)
  • file tersembunyi, yang dimulai dengan sebuah titik (jika glob adalah *.logbukanlog*
  • direktori yang cocok dengan glob (mis direktori logsyang cocok log*)
  • direktori kosong (yaitu hasilnya 0)
  • direktori yang sangat besar (daftar semuanya dapat menghabiskan memori)

Inilah solusi yang menangani semuanya:

ls 2>/dev/null -Ubad1 -- log* | wc -l

Penjelasan:

  • -Umenyebabkan lsuntuk tidak mengurutkan entri, artinya tidak perlu memuat seluruh daftar direktori dalam memori
  • -bmencetak gaya-C yang keluar untuk karakter nongrafik, yang paling penting menyebabkan baris baru dicetak sebagai \n.
  • -amencetak semua file, bahkan file yang tersembunyi (tidak sepenuhnya dibutuhkan ketika glob log*tidak mengandung file tersembunyi)
  • -dmencetak direktori tanpa berusaha membuat daftar isi direktori, yang lsbiasanya akan dilakukan
  • -1 memastikan bahwa itu ada di satu kolom (ls melakukan ini secara otomatis saat menulis ke pipa, sehingga tidak sepenuhnya diperlukan)
  • 2>/dev/nullmengarahkan ulang stderr sehingga jika ada 0 file log, abaikan pesan kesalahan. (Catatan yang shopt -s nullglobakan menyebabkan lsdaftar seluruh direktori kerja sebagai gantinya.)
  • wc -lmengkonsumsi daftar direktori seperti yang dihasilkan, jadi output dari lstidak pernah ada dalam memori kapan saja.
  • --Nama file dipisahkan dari perintah menggunakan --agar tidak dipahami sebagai argumen untuk ls(jika log*dihapus)

Shell akan diperluas log*ke daftar lengkap file, yang dapat menghabiskan memori jika banyak file, jadi menjalankannya melalui grep lebih baik:

ls -Uba1 | grep ^log | wc -l

Yang terakhir ini menangani direktori file yang sangat besar tanpa menggunakan banyak memori (meskipun menggunakan subkulit). Tidak -dlagi diperlukan, karena itu hanya daftar isi direktori saat ini.

mogsie
sumber
48

Untuk pencarian rekursif:

find . -type f -name '*.log' -printf x | wc -c

wc -cakan menghitung jumlah karakter dalam output find, sementara-printf x memberitahu finduntuk mencetak satu xuntuk setiap hasil.

Untuk pencarian non-rekursif, lakukan ini:

find . -maxdepth 1 -type f -name '*.log' -printf x | wc -c
Will Vousden
sumber
6
Bahkan jika Anda tidak memiliki file dengan spasi, beberapa pengguna skrip Anda mungkin menemukan file bernama jahat, menyebabkan skrip gagal. Juga, orang lain yang mengalami hal ini di StackOverflow mungkin memiliki file dengan baris baru, dan perlu mengetahui perangkapnya.
mogsie
FYI jika Anda meninggalkan -name '*.log'begitu saja maka itu akan menghitung semua file, yang saya butuhkan untuk kasus penggunaan saya. Bendera -maxdepth juga sangat berguna, terima kasih!
starmandeluxe
2
Ini masih menghasilkan hasil yang salah jika ada nama file dengan baris baru di dalamnya. Solusinya mudah dengan find; hanya mencetak sesuatu yang lain dari nama file kata demi kata.
tripleee
8

Jawaban yang diterima untuk pertanyaan ini salah, tetapi saya memiliki rep rendah sehingga tidak dapat menambahkan komentar untuk itu.

Jawaban yang benar untuk pertanyaan ini diberikan oleh Mat:

shopt -s nullglob
logfiles=(*.log)
echo ${#logfiles[@]}

Masalah dengan jawaban yang diterima adalah bahwa wc-l menghitung jumlah karakter baris baru, dan menghitungnya bahkan jika mereka mencetak ke terminal sebagai '?' dalam output 'ls-l'. Ini berarti bahwa jawaban yang diterima GAGAL ketika nama file berisi karakter baris baru. Saya telah menguji perintah yang disarankan:

ls -l log* | wc -l

dan secara keliru melaporkan nilai 2 bahkan jika hanya ada 1 file yang cocok dengan pola yang namanya mengandung karakter baris baru. Sebagai contoh:

touch log$'\n'def
ls log* -l | wc -l
Dan Yard
sumber
6

Jika Anda memiliki banyak file dan Anda tidak ingin menggunakan shopt -s nullglobsolusi array elegan dan bash, Anda dapat menggunakan find dan sebagainya selama Anda tidak mencetak nama file (yang mungkin berisi baris baru).

find -maxdepth 1 -name "log*" -not -name ".*" -printf '%i\n' | wc -l

Ini akan menemukan semua file yang cocok dengan log * dan yang tidak dimulai dengan .* - "not name. *" Redunant, tetapi penting untuk dicatat bahwa default untuk "ls" adalah tidak menampilkan file-file dot, tetapi default untuk menemukan adalah memasukkan mereka.

Ini adalah jawaban yang benar, dan menangani semua jenis nama file yang dapat Anda berikan padanya, karena nama file tidak pernah berpindah antar perintah.

Tapi, shopt nullglobjawabannya adalah jawaban terbaik!

mogsie
sumber
Anda mungkin harus memperbarui jawaban asli Anda alih-alih menjawab lagi.
qodeninja
Saya pikir menggunakan findvs menggunakan lsadalah dua cara berbeda untuk menyelesaikan masalah. findtidak selalu hadir pada mesin, tetapi lsbiasanya,
mogsie
2
Tetapi kemudian sebuah kotak lemak babi yang tidak memiliki findmungkin tidak memiliki semua opsi mewah untuk lskeduanya.
tripleee
1
Perhatikan juga bagaimana ini meluas ke seluruh pohon direktori jika Anda menghapus-maxdepth 1
tripleee
1
Catatan solusi ini akan menghitung file di dalam direktori tersembunyi dalam hitungannya. findmelakukan ini secara default. Ini dapat membuat kebingungan jika seseorang tidak menyadari ada folder anak tersembunyi, dan mungkin membuatnya menguntungkan untuk digunakan lsdalam beberapa keadaan, yang tidak melaporkan file tersembunyi secara default.
MrPotatoHead
6

Ini satu liner saya untuk ini.

 file_count=$( shopt -s nullglob ; set -- $directory_to_search_inside/* ; echo $#)
zee
sumber
Butuh beberapa googling untuk mengerti, tapi ini bagus! Jadi set -- tidak melakukan apa pun kecuali menyiapkan kita $#, yang menyimpan sejumlah argumen baris perintah yang diteruskan ke program shell
xverges
@ xverges Ya, "shopt -s nullglob" adalah untuk tidak menghitung file tersembunyi (.files). set - adalah untuk menyimpan / mengatur jumlah parameter posisi (jumlah file, dalam hal ini). dan # $ untuk menampilkan jumlah parameter posisi (jumlah file).
zee
3

Anda dapat menggunakan opsi -R untuk menemukan file bersama dengan yang ada di dalam direktori rekursif

ls -R | wc -l // to find all the files

ls -R | grep log | wc -l // to find the files which contains the word log

Anda dapat menggunakan pola pada grep

Moh
sumber
3

Komentar penting

(tidak cukup reputasi untuk berkomentar)

Ini BUGGY :

ls -1q some_pattern | wc -l

Jika shopt -s nullglobkebetulan diatur, itu mencetak jumlah SEMUA file biasa, bukan hanya yang dengan pola (diuji pada CentOS-8 dan Cygwin). Siapa yang tahu apa yang dilakukan bug tidak berarti lainnyals dimiliki ?

Ini BENAR dan jauh lebih cepat:

shopt -s nullglob; files=(some_pattern); echo ${#files[@]};

Itu melakukan pekerjaan yang diharapkan.


Dan waktu berlari berbeda.
Yang pertama: 0.006pada CentOS, dan 0.083pada Cygwin (kalau-kalau digunakan dengan hati-hati).
Yang ke-2: 0.000di CentOS, dan 0.003di Cygwin.

Anak kecil
sumber
2

Anda dapat mendefinisikan perintah seperti itu dengan mudah, menggunakan fungsi shell. Metode ini tidak memerlukan program eksternal dan tidak menelurkan proses anak. Itu tidak mencoba lsparsing berbahaya dan menangani karakter "khusus" (spasi putih, baris baru, garis miring terbalik dan sebagainya) baik-baik saja. Itu hanya bergantung pada mekanisme ekspansi nama file yang disediakan oleh shell. Ini kompatibel dengan setidaknya sh, bash, dan zsh.

Baris di bawah ini mendefinisikan fungsi yang disebut countyang mencetak jumlah argumen yang telah dipanggil.

count() { echo $#; }

Sebut saja dengan pola yang diinginkan:

count log*

Agar hasilnya benar ketika pola globbing tidak cocok, opsi shell nullglob(atau failglob- yang merupakan perilaku default pada zsh) harus ditetapkan pada saat ekspansi terjadi. Dapat diatur seperti ini:

shopt -s nullglob    # for sh / bash
setopt nullglob      # for zsh

Bergantung pada apa yang ingin Anda hitung, Anda mungkin juga tertarik dengan opsi shell dotglob.

Sayangnya, dengan bash setidaknya, tidak mudah untuk mengatur opsi ini secara lokal. Jika Anda tidak ingin mengaturnya secara global, solusi paling mudah adalah menggunakan fungsi ini dengan cara yang lebih berbelit-belit:

( shopt -s nullglob ; shopt -u failglob ; count log* )

Jika Anda ingin memulihkan sintaks yang ringan count log*, atau jika Anda benar-benar ingin menghindari memunculkan subkulit, Anda dapat meretas sesuatu di sepanjang baris:

# sh / bash:
# the alias is expanded before the globbing pattern, so we
# can set required options before the globbing gets expanded,
# and restore them afterwards.
count() {
    eval "$_count_saved_shopts"
    unset _count_saved_shopts
    echo $#
}
alias count='
    _count_saved_shopts="$(shopt -p nullglob failglob)"
    shopt -s nullglob
    shopt -u failglob
    count'

Sebagai bonus, fungsi ini lebih umum digunakan. Misalnya:

count a* b*          # count files which match either a* or b*
count $(jobs -ps)    # count stopped jobs (sh / bash)

Dengan mengubah fungsi menjadi file skrip (atau program C yang setara), dapat dipanggil dari PATH, ia juga dapat dibuat dengan program-program seperti finddan xargs:

find "$FIND_OPTIONS" -exec count {} \+    # count results of a search
Maëlan
sumber
2

Saya telah memberikan jawaban ini banyak pemikiran, terutama mengingat hal -hal yang jangan diurai . Pada awalnya, saya mencoba

<PERINGATAN! TIDAK BEKERJA>
du --inodes --files0-from=<(find . -maxdepth 1 -type f -print0) | awk '{sum+=int($1)}END{print sum}'
</ PERINGATAN! TIDAK BEKERJA>

yang bekerja jika hanya ada nama file seperti

touch $'w\nlf.aa'

tetapi gagal jika saya membuat nama file seperti ini

touch $'firstline\n3 and some other\n1\n2\texciting\n86stuff.jpg'

Saya akhirnya menemukan apa yang saya tuliskan di bawah. Catatan saya mencoba untuk mendapatkan hitungan semua file dalam direktori (tidak termasuk subdirektori). Saya pikir itu, bersama dengan jawaban oleh @Mat dan @Dan_Yard, serta memiliki setidaknya sebagian besar persyaratan yang ditetapkan oleh @mogsie (saya tidak yakin tentang memori.) Saya pikir jawabannya oleh @mogsie sudah benar, tetapi saya selalu berusaha untuk tidak menguraikan lskecuali itu situasi yang sangat spesifik.

awk -F"\0" '{print NF-1}' < <(find . -maxdepth 1 -type f -print0) | awk '{sum+=$1}END{print sum}'

Lebih mudah dibaca:

awk -F"\0" '{print NF-1}' < \
  <(find . -maxdepth 1 -type f -print0) | \
    awk '{sum+=$1}END{print sum}'

Ini melakukan pencarian khusus untuk file, membatasi output dengan karakter nol (untuk menghindari masalah dengan spasi dan linefeeds), lalu menghitung jumlah karakter nol. Jumlah file akan menjadi kurang dari jumlah karakter nol, karena akan ada karakter nol di akhir.

Untuk menjawab pertanyaan OP, ada dua hal yang perlu dipertimbangkan

1) Pencarian non-rekursif:

awk -F"\0" '{print NF-1}' < \
  <(find . -maxdepth 1 -type f -name "log*" -print0) | \
    awk '{sum+=$1}END{print sum}'

2) Pencarian rekursif. Perhatikan bahwa apa yang ada di dalam -nameparameter mungkin perlu diubah untuk perilaku yang sedikit berbeda (file tersembunyi, dll.).

awk -F"\0" '{print NF-1}' < \
  <(find . -type f -name "log*" -print0) | \
    awk '{sum+=$1}END{print sum}'

Jika ada yang ingin mengomentari bagaimana jawaban ini dibandingkan dengan yang saya sebutkan dalam jawaban ini, silakan lakukan.


Catatan, saya sampai pada proses pemikiran ini sambil mendapatkan jawaban ini .

bballdave025
sumber
1

Inilah yang selalu saya lakukan:

ls log * | awk 'END {print NR}'

Shuang Liang
sumber
awk 'END{print NR}'harus setara dengan wc -l.
musiphil
0
ls -1 log* | wc -l

Yang berarti daftar satu file per baris dan kemudian pipa ke perintah jumlah kata dengan pergantian parameter untuk menghitung baris.

nudzo
sumber
Opsi "-1" tidak diperlukan saat memipis keluaran ls. Tetapi Anda mungkin ingin menyembunyikan pesan kesalahan ls jika tidak ada file yang cocok dengan pola. Saya sarankan "ls log * 2> / dev / null | wc -l".
JohnMudd
Diskusi di bawah jawaban Daniel juga relevan di sini. Ini berfungsi dengan baik ketika Anda tidak memiliki direktori yang cocok atau nama file dengan baris baru, tetapi jawaban yang baik setidaknya harus menunjukkan kondisi batas ini, dan jawaban yang bagus seharusnya tidak memilikinya. Banyak bug karena seseorang menyalin / menempelkan kode yang tidak mereka mengerti; jadi menunjukkan kelemahan setidaknya membantu mereka memahami apa yang harus diperhatikan. (Memang, lebih banyak bug terjadi karena mereka mengabaikan peringatan dan kemudian hal-hal berubah setelah mereka pikir kode itu mungkin cukup baik untuk tujuan mereka.)
tripleee
-1

Untuk menghitung semuanya, pipa saja ke baris hitung kata:

ls | wc -l

Untuk menghitung dengan pola, pipa untuk menerima terlebih dahulu:

ls | grep log | wc -l
jturi
sumber