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:


11

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:

output=$(somecommand; echo .)
line_count=$(($(printf "%s\n" "$output" | wc -l) - 1))
printf "The exact output is:\n%s" "${output%.}"

1
@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,

string_variable="one
two
three
four"

Melakukan dengan grepmencocokkan mulai dari garis

printf '%s' "${string_variable}" | grep -c '^'
4

Dan dengan awk

printf '%s' "${string_variable}" | awk 'BEGIN { count=0 } NF { count++ } END { print count }'

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.

Lihat Bagaimana cara menggunakan POSIXLY_CORRECT di grep?


2
Tentunya wc -lmasih layak di sini?
Michael Homer

@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:

printf "%s\n" "$s" | wc -l         # 6
printf "%s"   "$s" | wc -l         # 5

Namun, harap perhatikan bahwa wchanya menghitung baris lengkap (atau jumlah karakter baris baru dalam string). grep -c ^juga harus menghitung fragmen baris terakhir.

s='foo'
printf "%s" "$s" | wc -l           # 0 !

printf "%s" "$s" | grep -c ^       # 1

(Tentu saja Anda juga bisa menghitung garis seluruhnya dalam shell dengan menggunakan ${var%...}ekspansi untuk menghapusnya satu per satu dalam satu lingkaran ...)


0

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:

lines() (
IFS='
'
set -f #disable pathname expansion
set -- $*
echo $#
)

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 -f
for line in $lines
do
    printf '[%s]\n' $line
done

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:

while IFS=$t read -r path scale
do
    ffmpeg -i "$path" ${scale:+-vf scale=$scale} "${path%.*}.out.mkv"
done <recode-queue.txt

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 -- $lines tidak 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.

Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.