Daftar Isi

Optimisasi Mikro dan Portabilitas dalam Shell-script #2

Shell-scripting mindset shift pt. 2 of 7


بِسْمِ ٱللَّٰهِ ٱلرَّحْمَٰنِ ٱلرَّحِيمِ

I/O Redirection feat. IFS

Tak sedikit dari kita terbiasa menggunakan cat (bahkan piping ke grep) untuk membaca dan/atau mengurai file. Namun, pernahkan kalian memanfaatkan penuh redirection operator untuk mengalihkan I/O di beberapa shell seperti bash/zsh? Yap, merupakan salah satu fitur favorit penulis.

Clobbering Output

Singkatnya, clobbering adalah proses menimpa data sepenuhnya pada file maupun processor register atau sebuah wilayah dari memori komputer. Kalian pasti sudah terbiasa menggunakannya.

1
2
echo 'file standard input' > /tmp/file # Membuat dan menimpa file serta datanya.
echo '2nd line' >> /tmp/file # Menambah data di baris baru tanpa menimpanya.

POSIX.1-2017


Untuk mencengah clobbering, dapat dilakukan dengan mengatur parameter pada shell seperti set -o noclobber di ksh, bash, dan zsh. Lalu, set noclobber untuk csh dan tcsh. Sedangkan dash menggunakan set -C. Untuk mengatifkannya kembali, ubah karakter - menjadi + di opsi.

1
set -C # Argument List Processing, noclobber, `dash`.
1
echo 'overwrite the whole content' > /tmp/file
/proc/self/fd/2
dash: 2: cannot create /tmp/file: File exists

Namun, pengaturan parameter noclobber dapat diabaikan dengan operator >| agar melewatinya.


POSIX juga mendefinisikan file descriptor yang diwakili dengan angka desimal, dimulai dari 0 (setidaknya) sampai 9 untuk digunakan. Nilai 0 (stdin), 1 (stdout), dan 2 (stderr) memiliki arti khusus dan penggunaan konvensional, serta tersirat oleh operasi pengalihan tertentu. Mereka disebut sebagai standard input, standard output, dan standard error. Biasanya, program mengambil input dari stdin, menulis output pada stdout, dan menulis pesan kesalahan (atau error) pada stderr. Selain nilai tersebut, digunakan spesifik untuk kasus tertentu dan biasanya kompleks seperti reverse shell.


File Standard Input

1
< /tmp/file
/proc/self/fd/1
file standard input
2nd line

POSIX.1-2017


Here Documents Input

1
2
3
4
5
<<- "EOL"
	heredocs, multiple lines
	2nd line
	3rd line
EOL
Menggunakan ekspansi paramater, substitusi perintah, dan sebagainya.
Hilangkan tanda kutip ganda (") dari EOL, sehingga sintaks tidak diperlakukan sebagai string.
/proc/self/fd/1
heredocs, multiple lines
2nd line
3rd line

POSIX.1-2017


Here Strings Input

1
<<< 'herestrings, one-liner'
/proc/self/fd/1
herestrings, one-liner

Itu tidak didefinisikan oleh POSIX. Berlaku di ksh, bash, dan zsh. Bagaimana dengan shell yang lain?


Di zsh, tiga jenis input di atas dapat dieksekusi tanpa menggunakan perintah atau utilitas eksternal lain. Namun, di bash 4.4 atau lebih baru, itu perlu ditetapkan sebagai sebuah variabel terlebih dulu karena hanya mendukung di dalam substitusi perintah, serta hanya mendukung file standard input.

1
FILE="$(< /tmp/file)"
1
echo "$FILE"
/proc/self/fd/1
file standard input
2nd line

Adapun untuk POSIX-compliant sh yang lain di mana tidak mendukung fitur tersebut, kita dapat menggunakan perintah read bawaan untuk membaca (dan/atau mengurai) setiap baris dalam file.

1
read -r FILE < /tmp/file # Hanya membaca sebaris dari file standard input.
1
echo "$FILE"
/proc/self/fd/1
file standard input

Bagaimana untuk membaca (dan/atau mengurai) file dengan banyak baris dengan perintah read?

1
2
3
while read -r LINE; do
    echo "$LINE"
done < /tmp/file
/proc/self/fd/1
file standard input
2nd line
1
2
3
4
5
6
7
while read -r LINE; do
    echo "$LINE"
done <<- "EOL"
	heredocs, multiple lines
	2nd line
	3rd line
EOL
/proc/self/fd/1
heredocs, multiple lines
2nd line
3rd line

Namun, teknik tersebut tidak disarankan jika baris dalam file sangat banyak karena prosesnya lambat, mengingat itu membaca dan menampilkan output tiap baris secara berulang (atau looping).


Specific Use-case Initiative

Mengakali utilitas yang tidak mendukung I/O redirection dan input ganda.

Nah, setelah membahas dasar yang biasa digunakan, apakah kalian bertanya-tanya bahwa beberapa utilitas atau program tidak bekerja seperti apa yang diharapkan? Sebagai contoh, menggunakan heredocs untuk mengecualikan file pada perintah rsync dengan memanfaatkan file descriptor yang ada di Linux walau hanya bekerja jika yang diinginkan adalah input tunggal.

1
2
3
4
5
6
rsync -avxHAXP --exclude-from=/dev/stdin dotfiles/. ~/ << "EXCLUDE"
.git*
LICENSE
*.md
EXTRA_JOYFUL
EXCLUDE

Sebenarnya rsync mendukung - sebagai stdin atau stdout, itu tidak portabel antar program.


Untuk menggunakan output dari beberapa perintah sebagai input dari sebuah perintah tunggal, gunakanlah substitusi proses, itu akan membuat file descriptor khusus dan menggunakannya.

1
diff -u <(file /tmp/) <(file /var/tmp/)
/proc/self/fd/1
1
2
3
4
5
--- /proc/self/fd/11	2022-04-30 00:09:19.837762812 +0700
+++ /proc/self/fd/12	2022-04-30 00:09:19.839762812 +0700
@@ -1 +1 @@
-/tmp/: sticky, directory
+/var/tmp/: sticky, directory

Itu tidak didefinisikan oleh POSIX. Berlaku di ksh86, bash, dan zsh. Bagaimana shell yang lain?


Satu lagi yang perlu diketahui bahwa perintah tee (dari GNU coreutils) dapat menyalin input ke stdout dan juga menulisnya ke (lebih dari satu) file secara bersama-sama (simultaneously).

1
echo 'this line will be in various outputs' | tee /tmp/tee1 /tmp/tee2
/proc/self/fd/1
this line will be in various outputs
1
grep '' /tmp/tee* # Tampilkan data file dengan utilitas GNU `grep`.
/proc/self/fd/1
/tmp/tee1: this line will be in various files and output
/tmp/tee2: this line will be in various files and output

IFS Implementation of read

Sekarang, kita coba terapkan pada kasus nyata dengan memanfaatkan variabel internal $_ dan $IFS untuk mengurai data yang ada dalam file. Sebagai contoh, kita uraikan file /proc/version di Linux.

1
cat /proc/version # Tampilkan data awal dengan utilitas `cat`, GNU coreutils.
/proc/self/fd/1
Linux version 5.17.1-hikari-x86_64 (root@localh3art) (clang version 13.0.1, LLD 13.0.1) #1 SMP

Kemudian, urai untuk mengambil versi rilis kernel di mana setiap string dipisahkan dengan spasi.

1
IFS=' ' read -r _ _ KVER _ < /proc/version
1
echo "$KVER"
/proc/self/fd/1
5.17.1-hikari-x86_64

Seperti data awal yang disajikan, setiap string misalnya dari Linux ke version dipisahkan dengan spasi. Lalu, kita tetapkan parameter $IFS dengan nilai karakter spasi agar diperlakukan sebagai delimiter. Secara default, nilai dari parameter $IFS adalah spasi, tab, dan newline. Parameter $IFS sendiri sebenarnya berperilaku berbeda-beda di tiap sintaks. Karena kita membahas I/O redirection, kita tidak akan menjelaskan secara komprehensif perbedaannya dan fokus dengan perintah read.

Di dalam perintah read. Jika beberapa nama variabel ditentukan sebagai argumennya maka $IFS digunakan untuk membagi baris dari input, sehingga setiap variabel mendapatkan satu bidang dari input dan variabel terakhir mendapatkan semua bidang yang tersisa (jika ada lebih banyak bidang daripada variabel). Sehingga, untuk mengurai string Linux, version, dll (kita sebut bidang atau field) dengan delimiter karakter spasi, kita tentukan 2 bidang tersebut dengan nama variabel internal $_ yang tidak memiliki efek samping apa pun karena ditetapkan dari awal secara default. Sebenarnya bebas menentukan nama variabel apa pun. Secara default, nilai dari parameter $_ ditetapkan secara terus-menerus seiring perintah diinputkan dengan delimiter karakter spasi (atau pemisah argumen).


Contoh lain, kita uraikan file /proc/uptime, pengganti perintah uptime -p yang tidak portabel.

1
uptime -p # Argumen `-p` tidak ada di beberapa sistem operasi Unix-like.
/proc/self/fd/1
up 5 hours, 39 minutes
1
cat /proc/uptime # Tampilkan data awal dengan utilitas `cat`, GNU coreutils.
/proc/self/fd/1
20372.66 69096.54
Jumlah detik sistem telah aktif Jumlah detik total setiap core CPU saat sistem idle
20372.66 69096.54

Kemudian, urai dan proses dengan ekspansi aritmatika untuk menghasilkan format replika uptime.

1
IFS='.' read -r S _ < /proc/uptime
1
2
3
D="$((S/60/60/24))" # Kalkulasikan hari,
H="$((S/60/60%24))" # jam, dan
M="$((S/60%60))" # menit.
1
2
3
4
[ "$D" -lt 2 ] || DP='s' # Plural dalam Bahasa Inggris, hari (days),
[ "$H" -lt 2 ] || HP='s' # jam (hours),
[ "$M" -lt 2 ] || MP='s' # menit (minutes), dan
[ "$S" -lt 2 ] || SP='s' # detik (seconds).
1
2
3
4
[ "$D" -eq 0 ] || UPTIME_P="${D} day${DP}, " # Format `uptime -p`, hari,
[ "$H" -eq 0 ] || UPTIME_P="${UPTIME_P}${H} hour${HP}, " # jam,
[ "$M" -eq 0 ] || UPTIME_P="${UPTIME_P}${M} minute${MP}" # menit, dan
[  -n  "$M"  ] || UPTIME_P="${UPTIME_P}${S} second${SP}" # detik.
1
echo 'up' "$UPTIME_P"
/proc/self/fd/1
up 5 hours, 39 minutes

Lanjutkan ke Halaman

#1 #2 #3 #4 #5 #6 #7