Optimisasi Mikro dan Portabilitas dalam Shell-script

Sabtu, 11 Juni 2022

Pernahkah kamu menulis skrip shell murni tanpa mengeksekusi utilitas eksternal?


Tak sedikit dari kita ketika pertama kali mengenal shell terutama bash dalam memproses string sederhana, langsung tanpa pikir panjang piping ke executable (yang bukan fitur bawaan dari shell) bahkan sampai berkali-kali. Misalnya saja cat | grep | awk | tr | sed | cut, itu merupakan sebuah pemborosan pemrosesan tugas dengan memanggil banyak utilitas lain dalam pipeline walau sebenarnya sama sekali bukan masalah untuk kepraktisan di dalam mode interaktif shell.

Namun, bagaimana jika kita menerapkan metode boros tersebut secara berulang dalam sebuah skrip shell? Tentu saja akan berpengaruh pada waktu eksekusinya. Meski pengaruhnya tidak terlalu signifikan, alangkah baiknya kita memahami tentang optimisasi mikro (atau kecil) dalam menulis skrip shell menggunakan fitur bawaan shell khususnya POSIX-compliant sh. Kita juga akan sedikit membandingkan fitur-fitur bawaan shell yang berbeda dalam hal portabilitas serta alternatifnya.

Parameter Expansion

Apa itu ekspansi parameter?
Sudahkah mengerti apa yang dimaksud dengan parameter dalam shell?

Sebuah shell menyimpan satu set parameter. Parameter yang dinotasikan dengan nama disebut variabel. Saat pertama kali mengeksekusi shell (start-up), shell akan mengubah semua variabel lingkungan (yang ditetapkan oleh program lain) menjadi sebuah variabel dalam lingkungan shell.
$ nama=nilai

Positional Parameter

Parameter posisi adalah parameter yang dinotasikan dengan angka (n > 0). Sejak awal, shell menetapkan nilai tersebut ke argumen yang mengikuti nama shell maupun sebuah skrip, $0. Perintah set bawaan dapat digunakan untuk menetapkan atau meresetnya. Array, tapi satu array.

Special Parameter

Parameter khusus adalah parameter yang dinotasikan dengan karakter khusus seperti *, @, #, ?, - (hypen), $, dan !. Detailnya dapat kalian lihat lebih lanjut di manpage tiap-tiap shell yang digunakan.
$ man 1 bash | less +/Special\ Parameters +n

Sederhananya, ekspansi atau perluasan parameter dapat kita gunakan untuk memodifikasi maupun memperoleh informasi nilai dari sebuah parameter itu (seperti variabel). Formatnya sebagai berikut:
${ekspresi}
Ekspresi di atas dapat diisi semua karakter di dalam kurung kurawal pembuka ({) hingga penutup (}).

Substring Conditional Test

${parameter:-string}
POSIX.1-2017
Gunakan nilai ganti. Jika parameter tidak ditetapkan atau bernilai null, gunakan string sebagai nilai dari parameter
$ echo "${VAR:-"Ini adalah fallback dari nilai VAR yang tidak ditetapkan."}"

Ini adalah fallback dari nilai VAR yang tidak ditetapkan.

${parameter:?string}
POSIX.1-2017
Tampilkan string sebagai kesalahan (error) hanya jika parameter tidak ditetapkan atau bernilai null. 
$ echo "${VAR:?"Parameter VAR sama sekali belum, atau tidak ditetapkan!!!"}"

dash: 2: VAR: Parameter VAR sama sekali belum, atau tidak ditetapkan!!!

${parameter:=string}
POSIX.1-2017
Tetapkan nilai ganti. Jika parameter tidak ditetapkan atau bernilai null, tetapkan string sebagai nilai dari parameter. Fungsinya seperti ekspansi yang pertama, tetapi hanya perlu sekali digunakan karena menetapkan. Hanya berlaku untuk variabel, bukan parameter posisi atau parameter khusus. 

$ echo "${VAR:="val"}"

val

${parameter:+string}
POSIX.1-2017
Gunakan nilai alternatif. Jika parameter ditetapkan atau bernilai bukan null, gunakan string sebagai nilai dari parameter. Ekspansi ini merupakan kebalikan dari substring conditional test yang pertama.
$ echo "${VAR:+"Nilai dari VAR adalah $VAR."}"

Nilai dari VAR adalah val.

Dalam 4 format di atas, penggunaan tanda titik dua (:) setelah parameter digunakan untuk menguji parameter bernilai null. Jika tidak digunakan maka hanya menguji parameter yang tidak ditetapkan.

Substring Informant

${#parameter}
POSIX.1-2017
Menampikan panjang karakter string yang merupakan nilai dari parameter. Jika parameter adalah * atau @ maka output selalu 0. Jika parameter tidak ditetapkan dan set -u berlaku, ekspansi gagal. 
$ FAREWELL='Goodbye, world!'
$ echo "Panjang '$FAREWELL' adalah ${#FAREWELL} karakter."

Panjang ‘Goodbye, world!’ adalah 15 karakter.

Substring Processor 

${parameter%string}
POSIX.1-2017
Hapus pola dengan suffix terkecil. Untuk menghasilkan suatu pola (atau pattern), string tersebut harus diperluas. Ekspansi parameter kemudian akan menghasilkan parameter dengan bagian terkecil dari suffix (atau akhiran) yang cocok dengan pola yang dihapus.

${parameter%%string}
POSIX.1-2017
Hapus pola dengan suffix terbesar. Untuk menghasilkan suatu pola (atau pattern), string tersebut harus diperluas. Ekspansi parameter kemudian akan menghasilkan parameter dengan bagian terbesar dari suffix (atau akhiran) yang cocok dengan pola yang dihapus.

${parameter#string}
POSIX.1-2017
Hapus pola dengan prefix terkecil. Untuk menghasilkan suatu pola (atau pattern), string tersebut harus diperluas. Ekspansi parameter kemudian akan menghasilkan parameter dengan bagian terkecil dari prefix (atau awalan) yang cocok dengan pola yang dihapus.

${parameter##string}
POSIX.1-2017
Hapus pola dengan prefix terbesar. Untuk menghasilkan suatu pola (atau pattern), string tersebut harus diperluas. Ekspansi parameter kemudian akan menghasilkan parameter dengan bagian terbesar dari prefix (atau awalan) yang cocok dengan pola yang dihapus.

Dalam setiap kasus, notasi pencocokan pola (atau pattern matching) dengan glob (bukan regular expression) digunakan untuk mengevaluasi pola. Jika parameter adalah #, *, atau @ maka output ekspansi tidak ditentukan. Jika parameter tidak ditetapkan dan set -u berlaku, ekspansi gagal. Menyertakan string ekspansi parameter penuh dalam tanda kutip ganda (") tidak akan menyebabkan 4 jenis karakter pola (POSIX) di atas dikutip, sedangkan karakter tanda kutip di dalam kurung memiliki efek ini. Dalam setiap variasi, jika string dihilangkan maka pola kosong digunakan.
$ AV="$(amixer sget Master)"

$ echo "$AV" # Tampilkan data awal.

Simple mixer control ‘Master’,0
Capabilities: pvolume pswitch pswitch-joined
Playback channels: Front Left - Front Right
Limits: Playback 0 - 65536
Mono:
Front Left: Playback 39324 [60%] [on]
Front Right: Playback 39324 [60%] [on]
$ AV="${AV#* \[}" AV="${AV%%%\] *}"

$ echo "$AV" # Tampilkan data akhir.

60

${parameter:offset}
${parameter:offset:length}
Diimplementasikan di ksh93. Juga di bash, zsh, mksh, dan busybox sh.

Ekspansi substring. Perluas hingga panjang karakter nilai dari parameter, mulai dari karakter yang ditentukan oleh offset. Jika length dihilangkan maka akan diperluas ke substring parameter, mulai dari karakter yang ditentukan oleh offset, length dan offset adalah ekspresi aritmatika. length harus mengevaluasi ke angka yang lebih besar dari, atau sama dengan nol. Jika offset mengevaluasi ke angka yang kurang dari nol maka nilainya digunakan sebagai offset dari akhir nilai parameter. Jika parameter adalah @ maka hasilnya adalah parameter posisi length yang dimulai dari offset. Jika parameter adalah nama array terindeks yang disubskrip oleh @ atau * maka hasilnya adalah length anggota array yang diawali dengan ${parameter:offset}. offset negatif diambil relatif terhadap satu lebih besar dari indeks maksimum dari array yang ditentukan. Ekspansi substring yang diterapkan ke array asosiatif menghasilkan output ekspansi tidak ditentukan. Perhatikan bahwa offset negatif harus dipisahkan dari titik dua (:) setidaknya satu spasi untuk menghindari kebingungan dengan ekspansi :- dari substring conditional test. Pengindeksan substring berbasis 0 kecuali jika parameter posisi digunakan, dalam hal ini pengindeksan dimulai dari 1 secara default. Jika offset adalah nol dan parameter posisi digunakan maka parameter posisi $0 diawali ke daftar.
$ ANJAY='mabar'

$ printf '%s\n' "${ANJAY:2}" "${ANJAY:0:2}"

bar
ma

$ expr "x$ANJAY" : 'x.\{0,2\}\(.\{0,3\}\)' # Alternatif: `expr`, GNU coreutils.
bar

$ cut -b1-2 <<< "$ANJAY" # Alternatif: `cut`, GNU coreutils. Dari herestrings.

ma

${parameter/pattern}
${parameter/pattern/string}
${parameter//pattern}
${parameter//pattern/string}
Diimplementasikan di ksh93. Juga di bash, zsh, dan mksh.

Substitusi pola (atau pattern). Ekspansi parameter dan kecocokan pattern terpanjang terhadap nilainya diganti dengan string. Jika pattern dimulai dengan / maka semua kecocokan pattern yang pertama kali cocok akan diganti dengan string, dan // untuk mengganti semua yang cocok dengan string. Jika pattern dimulai dengan # maka itu harus cocok di awal nilai dari parameter. Jika pattern dimulai dengan % maka itu harus cocok di akhir nilai dari parameter. Jika string adalah null maka kecocokan pattern akan dihapus. Jika parameter adalah @ atau * maka operasi substitusi diterapkan ke setiap parameter posisi secara bergantian dan ekspansi adalah daftar yang dihasilkan. Jika parameter adalah variabel array yang disubskrip dengan @ atau * maka operasi substitusi diterapkan ke setiap anggota array secara bergantian dan ekspansi adalah daftar yang dihasilkan.
$ SONG="$(mpc current)"
$ printf '%s\n' "${SONG//[AaIiUuEeOo]}" "${SONG/- */- Dekat dengan Pikiranmu}"
nty-Gn - Cls t yr Mnd
Unity-Gain - Dekat dengan Pikiranmu
$ sed 's/- .*/- Dekat dengan Pikiranmu/' <<< "$SONG" # GNU `sed`. Dari herestrings.
Unity-Gain - Dekat dengan Pikiranmu

${parameter^pattern}
${parameter^^pattern}
${parameter,pattern}
${parameter,,pattern}
${parameter~~pattern}
Diimplementasikan di bash 4.3 atau lebih baru.

Modifikasi case. Ekspansi ini mengubah case karakter alfabet dalam parameter. Operator ^ mengonversi pencocokan pola (atau pattern matching) yang semula huruf kecil menjadi huruf besar, sedangkan operator , mengonversi huruf besar yang cocok menjadi huruf kecil. Ekspansi ^^, ,,, dan ~~ mengonversi setiap karakter yang cocok, sedangkan ekspansi ^, ,, dan ~ hanya mengonversi karakter pertama dalam nilai dari parameter. Ekspansi ~ dan ~~ untuk mengonversi setiap karakter besar dan kecil menjadi sebaliknya. Jika pattern dihilangkan maka itu diperlakukan seperti ? yang cocok dengan setiap karakter. Jika parameter adalah @ atau * maka operasi modifikasi case diterapkan ke setiap parameter posisi secara bergantian dan ekspansi adalah daftar yang dihasilkan. Jika parameter adalah variabel array yang disubskrip dengan @ atau * maka operasi modifikasi case diterapkan ke setiap anggota array secara bergantian dan ekspansi adalah daftar yang dihasilkan.
$ SONG="$(mpc current)"
$ printf '%s\n' "${SONG^^}" "${SONG,,}" "${SONG~~}"
SIGNUM/II (W/ IRUS & YOUSUKE SAITO) - FIRST NOTE
signum/ii (w/ irus & yousuke saito) - first note
SIGNUM/II (W/ iRUS & yOUSUKE sAITO) - fIRST NOTE
$ tr '[:lower:]' '[:upper:]' <<< "$SONG" # POSIX `tr`. Dari herestrings.
$ tr '[:upper:]' '[:lower:]' <<< "$SONG"
SIGNUM/II (W/ IRUS & YOUSUKE SAITO) - FIRST NOTE
signum/ii (w/ irus & yousuke saito) - first note
$ awk '{print toupper($0)}' <<< "$SONG" # POSIX `awk`. Dari herestrings.
$ awk '{print tolower($0)}' <<< "$SONG"
SIGNUM/II (W/ IRUS & YOUSUKE SAITO) - FIRST NOTE
signum/ii (w/ irus & yousuke saito) - first note
$ sed 's/.*/\U&/' <<< "$SONG" # GNU `sed`. Dari herestrings.
$ sed 's/.*/\L&/' <<< "$SONG"
$ sed 's/.*/\u&/' <<< "$SONG# Hanya mengonversi karakter pertama.
$ sed 's/.*/\l&/' <<< "$SONG"
SIGNUM/II (W/ IRUS & YOUSUKE SAITO) - FIRST NOTE
signum/ii (w/ irus & yousuke saito) - first note
Signum/ii (w/ Irus & Yousuke Saito) - First note
signum/ii (w/ Irus & Yousuke Saito) - First note
$ printf '%s\n' "${(U)SONG}" "${(L)SONG}" # Parameter Expansion Flags, `zsh` 5+.
$ printf '%s\n' "${SONG:u}" "${SONG:l}" # Parameter Expansion Modifiers, `zsh` 5+.
SIGNUM/II (W/ IRUS & YOUSUKE SAITO) - FIRST NOTE
signum/ii (w/ irus & yousuke saito) - first note
SIGNUM/II (W/ IRUS & YOUSUKE SAITO) - FIRST NOTE
signum/ii (w/ irus & yousuke saito) - first note

Sebenarnya masih terdapat beberapa yang spesifik, fitur spesifik bash seperti ${!prefix*} dan ${!name[@]} tidak akan dibahas lebih lanjut karena lumayan rumit, silahkan lihat manpage bash.
$ man 1 bash | less +/prefixed\ to\ the\ list.

Brace Expansion

Ekspansi kurung kurawal (atau brace) adalah mekanisme di mana string sewenang-wenang dapat dihasilkan. Namun, ekspansi ini tidak didefinisikan oleh POSIX dan hanya berlaku di bash, zsh, ksh93, yash, dan mksh. Ekspansi kurung kurawal ini memungkinkan untuk bersarang (atau nested) dan hasil dari setiap string yang diperluas tidak diurutkan, urutan kiri ke kanan akan dipertahankan.
$ echo Y{,N{,T{,K{,T}}}}S
YS YNS YNTS YNTKS YNTKTS

Selain format di atas, terdapat ekpresi urutan peningkatan (atau incremental sequence expression).
{x..y..incr}
Di mana x dan y adalah integer atau karakter alfabet tunggal, dan incr adalah nilai dari increment. Ketika bilangan bulat diberikan, ekspresi diperluas ke setiap angka antara x dan y. Bilangan bulat dapat diawali dengan nol untuk memaksa setiap suku memiliki lebar yang sama. Ketika x atau y dimulai dengan nol, shell mencoba untuk memaksa semua yang dihasilkan mengandung jumlah digit yang sama. Ketika karakter alfabet diberikan, ekspresi diperluas ke setiap karakter secara leksikografis antara x dan y. Perhatikan bahwa x dan y harus bertipe data sama. Ketika increment diberikan, itu digunakan sebagai selisih antar setiap angka. Default dari increment adalah 1 atau -1.
$ echo {A..C} {0..100..10}
A B C 0 10 20 30 40 50 60 70 80 90 100

Ekspansi kurung kurawal dilakukan sebelum ekspansi lainnya dan setiap karakter khusus untuk ekspansi lain dipertahankan. Ekspansi kurung kurawal yang ditulis dengan benar harus berisi kurung kurawal buka ({) dan kurung tutup (}), dan setidaknya satu koma tanpa kutip atau ekspresi urutan peningkatan yang valid. Ekspansi kurung kurawal yang salah format dibiarkan tidak berubah.
$ ls /var/{,tmp/}
/var/:
cache db empty lib lock log run spool tmp

/var/tmp/:
ccache portage

...

Hampir dua ribu kata tertulis ... akan terlalu banyak jika dimuat di sini.
Oleh karena itu, pembahasan komprehensif lebih lanjut dapat dibaca di sini.

Daftar yang dibahas mencakup:

Mengapa dengan semua ini?

Artikel Terkait Bash ,GNU/Linux ,Shell