Bagaimana POSIX-ly menghitung jumlah baris dalam variabel string?
10
Saya tahu saya bisa melakukan ini di Bash:
wc -l <<<"${string_variable}"
Pada dasarnya, semua yang saya temukan melibatkan <<<operator Bash.
Tetapi dalam shell POSIX, <<<tidak terdefinisi, dan saya tidak dapat menemukan pendekatan alternatif selama berjam-jam. Saya cukup yakin ada solusi sederhana untuk ini, tetapi sayangnya, saya belum menemukannya sejauh ini.
Jawaban sederhananya adalah wc -l <<< "${string_variable}"pintas ksh / bash / zsh untuk printf "%s\n" "${string_variable}" | wc -l.
Sebenarnya ada perbedaan dalam cara <<<dan pekerjaan pipa: <<<membuat file sementara yang dikirimkan sebagai input ke perintah, sedangkan |membuat pipa. Dalam bash dan pdksh / mksh (tetapi tidak di ksh93 atau zsh), perintah di sisi kanan pipa berjalan dalam subkulit. Tetapi perbedaan-perbedaan ini tidak penting dalam kasus khusus ini.
Perhatikan bahwa dalam hal menghitung garis, ini mengasumsikan bahwa variabel tidak kosong dan tidak berakhir dengan baris baru. Tidak berakhir dengan baris baru adalah kasus ketika variabel adalah hasil dari substitusi perintah, sehingga Anda akan mendapatkan hasil yang benar dalam banyak kasus, tetapi Anda akan mendapatkan 1 untuk string kosong.
Ada dua perbedaan antara var=$(somecommand); wc -l <<<"$var"dan somecommand | wc -l: menggunakan substitusi perintah dan variabel sementara menghapus baris kosong di bagian akhir, lupa apakah baris terakhir dari output berakhir pada baris baru atau tidak (itu selalu terjadi jika perintah menghasilkan file teks kosong yang valid) , dan overcounts oleh satu jika output kosong. Jika Anda ingin mempertahankan hasil dan menghitung baris, Anda dapat melakukannya dengan menambahkan beberapa teks yang dikenal dan menghapusnya di akhir:
@Inian Keeping wc -lpersis sama dengan aslinya: <<<$foomenambahkan baris baru ke nilai $foo(meskipun $fookosong). Saya menjelaskan dalam jawaban saya mengapa ini mungkin bukan yang diinginkan, tetapi itulah yang ditanyakan.
Gilles 'SANGAT berhenti menjadi jahat'
2
Tidak sesuai dengan built-in shell, menggunakan utilitas eksternal seperti grepdan awkdengan opsi yang sesuai dengan POSIX,
Perhatikan bahwa beberapa alat GNU, khususnya, GNU greptidak menghargai POSIXLY_CORRECT=1opsi untuk menjalankan versi POSIX dari alat tersebut. Dalam grepsatu-satunya perilaku yang dipengaruhi oleh pengaturan variabel akan menjadi perbedaan dalam pemrosesan urutan bendera baris perintah. Dari dokumentasi ( grepmanual GNU ), tampaknya itu
POSIXLY_CORRECT
Jika diatur, grep berlaku seperti yang diminta POSIX; jika tidak, grepberperilaku lebih seperti program GNU lainnya. POSIX mensyaratkan bahwa opsi yang mengikuti nama file harus diperlakukan sebagai nama file; secara default, opsi tersebut diijinkan ke bagian depan daftar operan dan diperlakukan sebagai opsi.
@MichaelHomer: Dari apa yang saya amati, wc -lperlu aliran dibatasi baris baru yang tepat (memiliki trailing '\ n` di akhir untuk menghitung dengan benar). Seseorang tidak dapat menggunakan FIFO sederhana untuk digunakan printf, misalnya printf '%s' "${string_variable}" | wc -lmungkin tidak bekerja seperti yang diharapkan tetapi <<<akan karena \njejak ditambahkan oleh herestring
Inian
1
Itulah yang printf '%s\n'sedang dilakukan, sebelum Anda mengeluarkannya ...
Michael Homer
1
String-sini <<<adalah versi satu-baris dari dokumen-sini <<. Yang pertama bukan fitur standar, tetapi yang terakhir adalah. Anda dapat menggunakannya <<juga dalam kasus ini. Ini harus setara:
wc -l <<<"$somevar"
wc -l << EOF
$somevar
EOF
Meskipun perlu dicatat bahwa keduanya menambahkan baris baru ekstra di akhir $somevar, misalnya ini dicetak 6, meskipun variabel hanya memiliki lima baris:
s=$'foo\n\n\nbar\n\n'
wc -l <<<"$s"
Dengan printf, Anda dapat memutuskan apakah Anda ingin tambahan baris baru atau tidak:
Namun, harap perhatikan bahwa wchanya menghitung baris lengkap (atau jumlah karakter baris baru dalam string). grep -c ^juga harus menghitung fragmen baris terakhir.
(Tentu saja Anda juga bisa menghitung garis seluruhnya dalam shell dengan menggunakan ${var%...}ekspansi untuk menghapusnya satu per satu dalam satu lingkaran ...)
Dalam kasus-kasus mengejutkan yang sering terjadi di mana apa yang sebenarnya perlu Anda lakukan adalah memproses semua baris yang tidak kosong di dalam suatu variabel dengan beberapa cara (termasuk menghitungnya), Anda dapat mengatur IFS menjadi hanya baris baru dan kemudian menggunakan mekanisme pemisahan kata shell untuk memecah baris yang tidak kosong terpisah.
Misalnya, inilah fungsi shell kecil yang menjumlahkan baris-baris tidak kosong di dalam semua argumen yang disediakan:
Tanda kurung, bukan kawat gigi, digunakan di sini untuk membentuk perintah majemuk untuk fungsi tubuh. Ini membuat fungsi dieksekusi dalam subkulit sehingga tidak mencemari pengaturan variabel IFS dan pathname dunia luar pada setiap panggilan.
Jika Anda ingin mengulang lebih dari baris yang tidak kosong, Anda dapat melakukannya dengan cara yang sama:
IFS='
'set-ffor line in $linesdo
printf '[%s]\n' $linedone
Memanipulasi IFS dengan cara ini adalah teknik yang sering diabaikan, juga berguna untuk melakukan hal-hal seperti parsing nama path yang dapat berisi spasi dari input kolom-dibatasi tab. Namun, Anda perlu menyadari bahwa dengan sengaja menghapus karakter spasi yang biasanya termasuk dalam pengaturan default space-tab-newline IFS dapat akhirnya menonaktifkan pemisahan kata di tempat-tempat di mana Anda biasanya berharap melihatnya.
Misalnya, jika Anda menggunakan variabel untuk membangun baris perintah yang rumit untuk sesuatu seperti ffmpeg, Anda mungkin ingin memasukkan -vf scale=$scalehanya ketika variabel scalediatur ke sesuatu yang tidak kosong. Biasanya Anda bisa mencapainya dengan ${scale:+-vf scale=$scale}tetapi jika IFS tidak menyertakan karakter spasi biasanya pada saat ekspansi parameter ini dilakukan, ruang antara -vfdan scale=tidak akan digunakan sebagai pemisah kata dan ffmpegakan dilewati -vf scale=$scalesebagai argumen tunggal, yang tidak akan mengerti.
Untuk memperbaiki itu, Anda akan lebih baik perlu memastikan IFS didirikan lebih normal sebelum melakukan ${scale}ekspansi, atau melakukan dua ekspansi: ${scale:+-vf} ${scale:+scale=$scale}. Kata pemisahan yang dilakukan shell dalam proses penguraian awal baris perintah, berbeda dengan pemisahan yang dilakukan selama fase ekspansi pemrosesan baris perintah tersebut, tidak bergantung pada IFS.
Hal lain yang bisa bernilai saat Anda akan melakukan hal semacam ini akan menciptakan dua variabel global shell untuk memegang hanya tab dan hanya baris baru:
t=' '
n='
'
Dengan begitu Anda bisa memasukkan $tdan $ndalam ekspansi di mana Anda membutuhkan tab dan baris baru, daripada membuang semua kode Anda dengan spasi kosong yang dikutip. Jika Anda lebih suka menghindari spasi yang dikutip sama sekali dalam cangkang POSIX yang tidak memiliki mekanisme lain untuk melakukannya, printfdapat membantu meskipun Anda memang perlu sedikit mengutak-atik untuk menghilangkan trailing baris baru dalam ekspansi perintah:
nt=$(printf '\n\t')
n=${nt%?}
t=${nt#?}
Kadang-kadang pengaturan IFS seolah-olah itu variabel lingkungan per-perintah berfungsi dengan baik. Misalnya, ini adalah loop yang membaca nama path yang diizinkan mengandung spasi dan faktor penskalaan dari setiap baris file input yang dibatasi-tab:
Dalam kasus ini, readbuiltin melihat IFS diatur menjadi hanya tab, sehingga tidak akan membagi jalur input yang dibaca di spasi juga. Tapi IFS=$t set -- $linestidak berhasil: shell mengembang $linessaat membangun setargumen builtin sebelum mengeksekusi perintah, sehingga pengaturan sementara IFS dengan cara yang hanya berlaku selama eksekusi builtin sendiri terlambat. Inilah sebabnya cuplikan kode yang saya berikan di atas semuanya mengatur IFS dalam langkah terpisah, dan mengapa mereka harus berurusan dengan masalah melestarikannya.
wc -l
persis sama dengan aslinya:<<<$foo
menambahkan baris baru ke nilai$foo
(meskipun$foo
kosong). Saya menjelaskan dalam jawaban saya mengapa ini mungkin bukan yang diinginkan, tetapi itulah yang ditanyakan.Tidak sesuai dengan built-in shell, menggunakan utilitas eksternal seperti
grep
danawk
dengan opsi yang sesuai dengan POSIX,Melakukan dengan
grep
mencocokkan mulai dari garisDan dengan
awk
Perhatikan bahwa beberapa alat GNU, khususnya, GNU
grep
tidak menghargaiPOSIXLY_CORRECT=1
opsi untuk menjalankan versi POSIX dari alat tersebut. Dalamgrep
satu-satunya perilaku yang dipengaruhi oleh pengaturan variabel akan menjadi perbedaan dalam pemrosesan urutan bendera baris perintah. Dari dokumentasi (grep
manual GNU ), tampaknya ituLihat Bagaimana cara menggunakan POSIXLY_CORRECT di grep?
sumber
wc -l
masih layak di sini?wc -l
perlu aliran dibatasi baris baru yang tepat (memiliki trailing '\ n` di akhir untuk menghitung dengan benar). Seseorang tidak dapat menggunakan FIFO sederhana untuk digunakanprintf
, misalnyaprintf '%s' "${string_variable}" | wc -l
mungkin tidak bekerja seperti yang diharapkan tetapi<<<
akan karena\n
jejak ditambahkan oleh herestringprintf '%s\n'
sedang dilakukan, sebelum Anda mengeluarkannya ...String-sini
<<<
adalah versi satu-baris dari dokumen-sini<<
. Yang pertama bukan fitur standar, tetapi yang terakhir adalah. Anda dapat menggunakannya<<
juga dalam kasus ini. Ini harus setara:Meskipun perlu dicatat bahwa keduanya menambahkan baris baru ekstra di akhir
$somevar
, misalnya ini dicetak6
, meskipun variabel hanya memiliki lima baris:Dengan
printf
, Anda dapat memutuskan apakah Anda ingin tambahan baris baru atau tidak:Namun, harap perhatikan bahwa
wc
hanya menghitung baris lengkap (atau jumlah karakter baris baru dalam string).grep -c ^
juga harus menghitung fragmen baris terakhir.(Tentu saja Anda juga bisa menghitung garis seluruhnya dalam shell dengan menggunakan
${var%...}
ekspansi untuk menghapusnya satu per satu dalam satu lingkaran ...)sumber
Dalam kasus-kasus mengejutkan yang sering terjadi di mana apa yang sebenarnya perlu Anda lakukan adalah memproses semua baris yang tidak kosong di dalam suatu variabel dengan beberapa cara (termasuk menghitungnya), Anda dapat mengatur IFS menjadi hanya baris baru dan kemudian menggunakan mekanisme pemisahan kata shell untuk memecah baris yang tidak kosong terpisah.
Misalnya, inilah fungsi shell kecil yang menjumlahkan baris-baris tidak kosong di dalam semua argumen yang disediakan:
Tanda kurung, bukan kawat gigi, digunakan di sini untuk membentuk perintah majemuk untuk fungsi tubuh. Ini membuat fungsi dieksekusi dalam subkulit sehingga tidak mencemari pengaturan variabel IFS dan pathname dunia luar pada setiap panggilan.
Jika Anda ingin mengulang lebih dari baris yang tidak kosong, Anda dapat melakukannya dengan cara yang sama:
Memanipulasi IFS dengan cara ini adalah teknik yang sering diabaikan, juga berguna untuk melakukan hal-hal seperti parsing nama path yang dapat berisi spasi dari input kolom-dibatasi tab. Namun, Anda perlu menyadari bahwa dengan sengaja menghapus karakter spasi yang biasanya termasuk dalam pengaturan default space-tab-newline IFS dapat akhirnya menonaktifkan pemisahan kata di tempat-tempat di mana Anda biasanya berharap melihatnya.
Misalnya, jika Anda menggunakan variabel untuk membangun baris perintah yang rumit untuk sesuatu seperti
ffmpeg
, Anda mungkin ingin memasukkan-vf scale=$scale
hanya ketika variabelscale
diatur ke sesuatu yang tidak kosong. Biasanya Anda bisa mencapainya dengan${scale:+-vf scale=$scale}
tetapi jika IFS tidak menyertakan karakter spasi biasanya pada saat ekspansi parameter ini dilakukan, ruang antara-vf
danscale=
tidak akan digunakan sebagai pemisah kata danffmpeg
akan dilewati-vf scale=$scale
sebagai argumen tunggal, yang tidak akan mengerti.Untuk memperbaiki itu, Anda akan lebih baik perlu memastikan IFS didirikan lebih normal sebelum melakukan
${scale}
ekspansi, atau melakukan dua ekspansi:${scale:+-vf} ${scale:+scale=$scale}
. Kata pemisahan yang dilakukan shell dalam proses penguraian awal baris perintah, berbeda dengan pemisahan yang dilakukan selama fase ekspansi pemrosesan baris perintah tersebut, tidak bergantung pada IFS.Hal lain yang bisa bernilai saat Anda akan melakukan hal semacam ini akan menciptakan dua variabel global shell untuk memegang hanya tab dan hanya baris baru:
Dengan begitu Anda bisa memasukkan
$t
dan$n
dalam ekspansi di mana Anda membutuhkan tab dan baris baru, daripada membuang semua kode Anda dengan spasi kosong yang dikutip. Jika Anda lebih suka menghindari spasi yang dikutip sama sekali dalam cangkang POSIX yang tidak memiliki mekanisme lain untuk melakukannya,printf
dapat membantu meskipun Anda memang perlu sedikit mengutak-atik untuk menghilangkan trailing baris baru dalam ekspansi perintah:Kadang-kadang pengaturan IFS seolah-olah itu variabel lingkungan per-perintah berfungsi dengan baik. Misalnya, ini adalah loop yang membaca nama path yang diizinkan mengandung spasi dan faktor penskalaan dari setiap baris file input yang dibatasi-tab:
Dalam kasus ini,
read
builtin melihat IFS diatur menjadi hanya tab, sehingga tidak akan membagi jalur input yang dibaca di spasi juga. TapiIFS=$t set -- $lines
tidak berhasil: shell mengembang$lines
saat membangunset
argumen builtin sebelum mengeksekusi perintah, sehingga pengaturan sementara IFS dengan cara yang hanya berlaku selama eksekusi builtin sendiri terlambat. Inilah sebabnya cuplikan kode yang saya berikan di atas semuanya mengatur IFS dalam langkah terpisah, dan mengapa mereka harus berurusan dengan masalah melestarikannya.sumber