Jumlah karakter dalam output perintah shell

12

Saya menulis naskah yang perlu menghitung jumlah karakter dalam output perintah dalam satu langkah .

Misalnya, menggunakan perintah readlink -f /etc/fstabharus kembali 10karena output dari perintah itu adalah 10 karakter.

Ini sudah dimungkinkan dengan variabel tersimpan menggunakan kode berikut:

variable="somestring";
echo ${#variable};
# 10

Sayangnya, menggunakan rumus yang sama dengan string yang dihasilkan perintah tidak berfungsi:

${#(readlink -f /etc/fstab)};
# bash: ${#(readlink -f /etc/fstab)}: bad substitution

Saya mengerti adalah mungkin untuk melakukan ini dengan terlebih dahulu menyimpan output ke variabel:

variable=$(readlink -f /etc/fstab);
echo ${#variable};

Tapi saya ingin menghapus langkah ekstra.

Apakah ini mungkin? Lebih cocok dengan shell Almquist (sh) yang hanya menggunakan utilitas bawaan atau standar.

pengguna339676
sumber
1
Outputnya readlink -f /etc/fstabadalah 11 karakter. Jangan lupa baris baru. Kalau tidak, Anda akan melihat /etc/fstabluser@cern:~$ ketika Anda menjalankannya dari shell.
Phil Frost
@ PhilFrost Anda tampaknya memiliki prompt lucu, apakah Anda bekerja di CERN?
Dmitry Grigoryev

Jawaban:

9

Dengan GNU expr :

$ expr length + "$(readlink -f /etc/fstab)"
10

The +ada fitur khusus GNU expruntuk memastikan argumen berikutnya diperlakukan sebagai string bahkan jika hal itu terjadi untuk menjadi exprOperator seperti match, length, +...

Di atas akan menghapus setiap baris baru dari output. Untuk mengatasinya:

$ expr length + "$(readlink -f /etc/fstab; printf .)" - 2
10

Hasilnya dikurangi menjadi 2 karena baris baru terakhir readlinkdan karakter yang .kami tambahkan.

Dengan string Unicode, exprtampaknya tidak berfungsi, karena mengembalikan panjang string dalam byte alih-alih jumlah karakter (Lihat baris 654 )

$ LC_ALL=C.UTF-8 expr length ăaa
4

Jadi, Anda bisa menggunakan:

$ printf "ăaa" | LC_ALL=C.UTF-8 wc -m
3

POSIXLY:

$ expr " $(readlink -f /etc/fstab; printf .)" : ".*" - 3
10

Ruang sebelum substitusi perintah mencegah perintah dari crash dengan string start with -, jadi kita perlu mengurangi 3.

cuonglm
sumber
Terima kasih! Tampaknya contoh ketiga Anda berfungsi bahkan tanpa LC_ALL=C.UTF-8, yang secara signifikan menyederhanakan hal-hal jika pengkodean string tidak akan diketahui sebelumnya.
user339676
2
expr length $(echo "*")- tidak. Setidaknya menggunakan tanda kutip ganda: expr length "$(…)". Tapi ini menghilangkan baris baru dari perintah, ini adalah fitur substitusi perintah yang tidak bisa dihindari. (Anda dapat mengatasinya, tetapi kemudian jawabannya menjadi lebih kompleks.)
Gilles 'SO- stop being evil'
6

Tidak yakin bagaimana melakukan ini dengan shell bawaan ( meskipun Gnouc ) tetapi alat standar dapat membantu:

  1. Anda dapat menggunakan wc -mkarakter mana yang diperhitungkan. Sayangnya, ini juga menghitung baris terakhir sehingga Anda harus menyingkirkannya terlebih dahulu:

    readlink -f /etc/fstab | tr -d '\n' | wc -m
  2. Anda tentu saja dapat menggunakan awk

    readlink -f /etc/fstab | awk '{print length($0)}'
  3. Atau Perl

    readlink -f /etc/fstab | perl -lne 'print length'
terdon
sumber
Apakah yang Anda maksud expradalah built-in? Di shell yang mana?
mikeserv
5

Saya biasanya melakukannya seperti ini:

$ echo -n "$variable" | wc -m
10

Untuk melakukan perintah, saya akan menyesuaikannya seperti ini:

$ echo -n "$(readlink -f /etc/fstab)" | wc -m
10

Pendekatan ini mirip dengan apa yang Anda lakukan dalam 2 langkah, kecuali kami menggabungkannya menjadi satu baris.

slm
sumber
2
Anda harus menggunakan -msebagai gantinya -c. Dengan karakter unicode, pendekatan Anda akan rusak.
cuonglm
1
Kenapa tidak sederhana saja readlink -f /etc/fstab | wc -m?
Phil Frost
1
Mengapa Anda menggunakan metode yang tidak dapat diandalkan ini alih-alih ${#variable}? Setidaknya menggunakan tanda kutip ganda echo -n "$variable", tetapi ini masih gagal jika misalnya nilai variableadalah -e. Saat Anda menggunakannya dalam kombinasi dengan substitusi perintah, perlu diingat bahwa trailing baris baru dilucuti.
Gilles 'SANGAT berhenti menjadi jahat'
@ philfrost b / c apa yang saya tunjukkan dibangun dari apa yang sudah dipikirkan oleh op. Juga berfungsi untuk cmds yang dia mungkin telah setup sebelumnya di vars dan ingin kata penutup panjangnya. Juga terdon sudah memiliki contoh itu.
slm
1

Anda dapat memanggil utilitas eksternal (lihat jawaban lain), tetapi mereka akan membuat skrip Anda lebih lambat, dan sulit untuk memperbaiki saluran air.

Zsh

Di zsh, Anda bisa menulis ${#$(readlink -f /etc/fstab)}untuk mendapatkan panjang substitusi perintah. Perhatikan bahwa ini bukan panjang dari output perintah, itu adalah panjang dari output tanpa baris baru.

Jika Anda ingin panjang pasti dari output, output karakter non-newline tambahan di akhir, dan kurangi satu.

$((${#$(readlink -f /etc/fstab; echo .)} - 1))

Jika yang Anda inginkan adalah payload di output perintah, maka Anda perlu mengurangi dua di sini, karena outputnya readlink -fadalah jalur kanonik ditambah baris baru.

$((${#$(readlink -f /etc/fstab; echo .)} - 2))

Ini berbeda dari ${#$(readlink -f /etc/fstab)}kasus yang jarang tetapi mungkin terjadi di mana jalur kanonik itu sendiri berakhir di baris baru.

Untuk contoh khusus ini, Anda tidak memerlukan utilitas eksternal sama sekali, karena zsh memiliki konstruksi bawaan yang setara dengan readlink -f, melalui pengubah riwayat A.

echo /etc/fstab(:A)

Untuk mendapatkan panjangnya, gunakan pengubah riwayat dalam ekspansi parameter:

${#${:-/etc/fstab}:A}

Jika Anda memiliki nama file dalam variabel filename, itu akan menjadi ${#filename:A}.

Kerang Bourne / gaya POSIX

Tak satu pun dari shell Bourne / POSIX murni (Bourne, ash, mksh, ksh93, bash, yash ...) memiliki ekstensi serupa yang saya ketahui. Jika Anda perlu menerapkan substitusi parameter ke output substitusi perintah atau ke substitusi parameter sarang, gunakan tahapan berturut-turut.

Anda dapat memasukkan pemrosesan ke dalam fungsi jika diinginkan.

command_output_length_sans_trailing_newlines () {
  set -- "$("$@")"
  echo "${#1}"
}

atau

command_output_length () {
  set -- "$("$@"; echo .)"
  echo "$((${#1} - 1))"
}

tetapi biasanya tidak ada manfaatnya; kecuali dengan ksh93, yang menyebabkan garpu tambahan untuk dapat menggunakan output dari fungsi, sehingga membuat skrip Anda lebih lambat, dan jarang ada manfaat keterbacaan.

Sekali lagi, output dari readlink -fadalah jalur kanonik ditambah baris baru; jika Anda ingin panjang jalur kanonik, kurangi 2 bukannya 1 in command_output_length. Menggunakan command_output_length_sans_trailing_newlinesmemberikan hasil yang benar hanya ketika jalur kanonik itu sendiri tidak berakhir di baris baru.

Bytes vs karakter

${#…}seharusnya menjadi panjang dalam karakter, bukan dalam byte, yang membuat perbedaan dalam lokal multibyte. Versi terbaru dari ksh93, bash, dan zsh menghitung panjang karakter sesuai dengan nilai LC_CTYPEpada saat ${#…}konstruk diperluas. Banyak shell umum lainnya tidak benar-benar mendukung local multibyte: pada dash 0.5.7, mksh 46 dan posh 0.12.3, ${#…}mengembalikan panjang dalam byte. Jika Anda ingin panjang karakter dengan cara yang dapat diandalkan, gunakan wcutilitas:

$(readlink -f /etc/fstab | wc -m)

Selama $LC_CTYPEmenetapkan lokal yang valid, Anda dapat yakin bahwa ini akan salah (pada platform kuno atau terbatas yang tidak mendukung lokal multibyte) atau mengembalikan panjang karakter yang benar. (Untuk Unicode, "panjang karakter" berarti jumlah titik kode - jumlah mesin terbang adalah cerita lain, karena komplikasi seperti menggabungkan karakter.)

Jika Anda ingin panjang dalam byte, atur LC_CTYPE=Csementara, atau gunakan wc -csebagai ganti wc -m.

Menghitung byte atau karakter dengan wcmenyertakan baris baru yang tertinggal dari perintah. Jika Anda ingin panjang jalur kanonik dalam byte, itu

$(($(readlink -f /etc/fstab | wc -c) - 1))

Untuk mendapatkannya dalam karakter, kurangi 2.

Gilles 'SANGAT berhenti menjadi jahat'
sumber
@ cuonglm Tidak, Anda perlu mengurangi 1. echo .menambahkan dua karakter, tetapi karakter kedua adalah baris tambahan yang dilucuti oleh substitusi perintah.
Gilles 'SANGAT berhenti menjadi jahat'
Baris baru dari readlinkoutput, ditambah .dengan echo. Kami berdua setuju bahwa echo .menambahkan dua karakter tetapi baris baru yang tertinggal dihapus. Coba dengan printf .atau lihat jawaban saya unix.stackexchange.com/a/160499/38906 .
cuonglm
@cuonglm. Pertanyaan ini menanyakan jumlah karakter dalam output perintah. Output dari readlinkadalah target tautan ditambah baris baru.
Gilles 'SO- stop being evil'
0

Ini berfungsi dashtetapi itu mengharuskan var yang ditargetkan pasti kosong atau tidak disetel. Itulah sebabnya ini sebenarnya dua perintah - saya secara eksplisit mengosongkan $lyang pertama:

l=;printf '%.slen is %d and result is %s\n' \
    "${l:=$(readlink -f /etc/fstab)}" "${#l}" "$l"

KELUARAN

len is 10 and result is /etc/fstab

Itu semua shell builtin - tidak termasuk readlinktentu saja - tetapi mengevaluasinya dalam shell saat ini dengan cara itu menyiratkan bahwa Anda harus melakukan tugas sebelum mendapatkan len, itulah sebabnya saya %.smelihat argumen pertama dalam printfstring format dan hanya menambahkannya lagi untuk nilai literal pada bagian akhir printfdaftar arg.

Dengan eval:

l=$(readlink -f /etc/fstab) eval 'l=${#l}:$l'
printf %s\\n "$l"

KELUARAN

10:/etc/fstab

Anda bisa mendekati hal yang sama, tetapi alih-alih output dalam variabel pada perintah pertama Anda mendapatkannya di stdout:

PS4='${#0}:$0' dash -cx '2>&1' "$(readlink -f /etc/fstab)"

... yang menulis ...

10:/etc/fstab

... untuk mengajukan deskriptor 1 tanpa memberikan nilai apa pun ke vars apa pun di shell saat ini.

mikeserv
sumber
1
Bukankah itu tepatnya yang ingin dihindari OP? "Saya mengerti adalah mungkin untuk melakukan ini dengan terlebih dahulu menyimpan output ke variabel: variable=$(readlink -f /etc/fstab); echo ${#variable};Tapi saya ingin menghapus langkah ekstra."
terdon
@terdon, mungkin saya salah paham, tetapi kesan saya bahwa titik koma adalah masalahnya dan bukan variabelnya. Itu sebabnya ini mendapatkan len dan output dalam satu perintah sederhana dengan hanya menggunakan builtin shell. Shell tidak exec readlink lalu exec expr, misalnya. Ini mungkin hanya masalah jika entah bagaimana mendapatkan len menyumbat nilai, yang saya akui saya mengalami kesulitan memahami mengapa itu mungkin terjadi, tetapi saya curiga mungkin ada kasus di mana itu penting.
mikeserv
1
The evalway, by the way, mungkin terbersih di sini - itu memberikan output dan len ke nama var yang sama di eksekusi tunggal - sangat dekat dengan melakukan l=length(l):out(l). Ngomong-ngomong, melakukan expr length $(command) melakukan oklusi nilai yang mendukung len.
mikeserv