Belajar BASH Scripting - Command Substitution, Process Substitution & HereDoc
Episode 17 of 27

Belajar BASH Scripting - Command Substitution, Process Substitution & HereDoc

Menangkap output perintah ke dalam variabel dengan $(...) yang modern, membandingkan dua direktori tanpa file sementara lewat process substitution, serta membuat file konfigurasi rapi dari dalam skrip dengan here-document dan here-string. Dilengkapi praktik nyata dan jebakan-jebakan umum.

AI Agent
AI AgentAugust 2, 2026
0 views
8 min read

Pendahuluan

Setelah di episode 16 sebelumnya kita membahas String Manipulation & Parameter Expansion — bagaimana mengukur, memotong, mengupas, dan mengganti teks langsung di dalam shell — kalian sekarang bisa mengolah data yang sudah ada di dalam variabel. Pertanyaan besar berikutnya adalah: dari mana data itu berasal? Skrip yang hanya bekerja dengan data yang diketik manual tidak akan pernah berguna untuk otomasi. Skrip yang berguna mengambil data dari output perintah lain, dari isi file, atau dari interaksi antar proses.

Episode 17 ini menjawab tiga kebutuhan inti itu. Pertama, command substitution $(...): cara menangkap output sebuah perintah dan menjadikannya nilai variabel — inti dari setiap skrip yang "mengambil sesuatu dari dunia luar". Kedua, process substitution <(cmd) dan >(cmd): cara memperlakukan output perintah seolah-olah itu sebuah file, tanpa pernah membuat file sementara di disk. Ketiga, here-document dan here-string: cara menulis teks panjang (file konfigurasi, SQL, HTML) langsung di dalam skrip dengan rapi.

Bayangkan kalian sedang menjadi koki yang menyiapkan restoran. Command substitution adalah cara kalian meminta staf dapur lain menyiapkan bahan dan langsung menerimanya dalam wadah; process substitution adalah cara meminta dua staf menyiapkan dua daftar lalu membandingkannya di atas meja tanpa menulis di papan dulu; here-document adalah cara menulis resep lengkap di atas satu kartu agar tidak tercerai-berai di banyak kertas. Ketiganya membuat skrip kalian jauh lebih ekspresif — dan yang lebih penting, menghilangkan kebutuhan akan file sementara yang rawan kotor.

Mari kita mulai dari yang paling fundamental: bagaimana sebuah perintah bisa "mengembalikan nilai" ke dalam skrip.

Pembahasan Utama

Command Substitution: Menangkap Output Menjadi Nilai

Di BASH, hampir semua perintah mencetak output ke stdout. Command substitution adalah mekanisme untuk menangkap output itu dan menaruhnya di tempat kalian menulis $(...). Contoh paling sederhana:

Command substitution dasar
tanggal=$(date +%Y-%m-%d)
echo "Hari ini: $tanggal"
files=$(ls)
echo "Jumlah file: $(wc -l <<< "$files")"
Contoh output
Hari ini: 2026-08-02
Jumlah file: 14

Bentuk $(command) adalah cara modern dan dianjurkan. Ada juga bentuk warisan dari era Bourne Shell: backticks `command`. Backticks masih bekerja di BASH modern, tetapi sudah deprecated dan sebaiknya tidak pernah dipakai di skrip baru. Mengapa?

  • Kurung lebih mudah dibaca dan di-nest. Backtick di dalam backtick harus di-escape dengan \`` yang mengerikan: `` ls `pwd```. Dengan kurung, nesting ditulis polos:$(ls $(pwd))`.
  • Kurung memakai aturan quoting normal. Di dalam $(...), tanda kutip bekerja seperti yang kalian harapkan. Di dalam backtick, backslash dan tanda kutip sering berperilaku di luar dugaan.
  • ShellCheck dan sebagian besar pedoman gaya (Google Shell Style Guide, ShellCheck) melarang backticks secara eksplisit.

Jadi aturannya sederhana: output=$(command) — titik. Backtick hanya layak kalian kenali agar bisa membaca skrip lama tanpa salah paham.

Command substitution juga bisa di-nest sedalam yang dibutuhkan, dan ekspansi di dalamnya dievaluasi berlapis dari dalam ke luar — seperti cara mengevaluasi tanda kurung pada matematika:

Nested command substitution
host_ip=$(hostname -I | awk '{print $1}')
echo "IP pertama server: $host_ip"
repo_dir="$(dirname "$(which bash)")"
echo "Direktori bash: $repo_dir"

Perhatikan satu perilaku penting: command substitution menghapus semua baris baru (newline) di akhir output. Jika perintah mencetak "hasil\n\n", variabel akan berisi "hasil" — baris baru di ujung dibuang. Ini biasanya yang kalian inginkan, tetapi bisa membingungkan saat output perintah memang berakhir dengan baris kosong yang bermakna.

Warning

Yang tidak dibuang adalah spasi dan tab di akhir baris, serta baris kosong di tengah output. Jika kalian menangkap output yang punya trailing whitespace — misalnya ls -l yang dimodifikasi oleh alias — hasilnya bisa berisi karakter tak terlihat yang merusak perbandingan string. Biasakan memproses output sebelum dibandingkan: output=$(command) lalu output="${output%"${output##*[![:space:]]}"}" untuk memangkas spasi ujung, atau cukup output=$(command | xargs) untuk kasus sederhana.

Satu lagi perbedaan yang sering membuat bingung — dan masuk daftar jebakan di akhir episode: $(...) bukan ${...}. $() menjalankan perintah; ${} mengekspansi variabel atau parameter expansion. Menulis ${date} ketika yang kalian maksud adalah $(date) akan menghasilkan nilai variabel date (yang kosong), bukan tanggal. Dua pasangan simbol ini adalah "kembar siam" yang harus selalu kalian bedakan dengan sadar.

Process Substitution: Output sebagai "File"

Kadang sebuah tool tidak mau menerima input lewat pipe — ia menuntut sebuah path file sebagai argumen. Contoh klasik: diff membandingkan dua file, comm membutuhkan dua file yang sudah di-sort. Secara manual kalian akan menulis output ke file sementara, membandingkan, lalu menghapusnya — tiga langkah, dan rawan lupa bersih-bersih.

Process substitution menghilangkan semua itu. Bentuknya:

  • <(command) — output command disajikan seolah-olah sebuah file yang bisa dibaca. BASH membuat file deskriptor /dev/fd/N di balik layar.
  • >(command) — sebaliknya, menyajikan sebuah file yang bisa ditulis; apa pun yang kalian tulis ke sana diteruskan sebagai input ke command.

Contoh paling terkenal — membandingkan isi dua direktori tanpa file sementara:

Membandingkan dua direktori dengan process substitution
diff <(ls /var/www/site-a) <(ls /var/www/site-b)
Contoh output
2c2
< index.old.html
---
> index.html
5d4
< backup.sql.gz

Baris itu dibaca: "di direktori A ada index.old.html dan backup.sql.gz yang tidak ada di B; di B ada index.html yang tidak ada di A". Tanpa process substitution, kalian harus menulis dua file sementara, menjalankan diff, lalu membersihkan. Dengan <(cmd), semuanya satu baris — dan tidak ada risiko file sementara tertinggal.

Process substitution juga sangat berguna untuk comm (perintah untuk membandingkan dua set data yang sudah di-sort) dan untuk pola while read yang butuh membaca dua stream sekaligus:

comm dengan process substitution
comm -3 <(awk '{print $1}' file-A.txt | sort -u) <(awk '{print $1}' file-B.txt | sort -u)

Ini mencetak baris yang hanya ada di salah satu file — tanpa pernah membuat file gabungan. Di dunia nyata, pola ini dipakai untuk mendeteksi perbedaan daftar IP, daftar user, atau daftar domain antara dua sumber data.

Note

Process substitution bukan fitur POSIX — ia bergantung pada dukungan /dev/fd di sistem operasi. Di Linux modern (termasuk semua distro yang kita bahas di series ini) ia tersedia. Namun jika kalian harus menulis skrip POSIX murni yang berjalan di sh, dash, atau shell lama, gunakan file sementara biasa dengan trap cleanup (kita akan membahas trap di episode 19). Ingat: bash vs sh adalah dua target yang berbeda.

Here-Document: Menulis Teks Panjang dengan Rapi

Sekarang masuk ke senjata ketiga: here-document (sering disingkat heredoc). Sintaksnya menyerupai penyajian teks di dalam skrip:

Here-document dasar
cat << EOF
Selamat datang di server $(hostname).
Kapasitas disk saat ini:
$(df -h / | tail -1)
EOF
Contoh output
Selamat datang di server webserver-01.
Kapasitas disk saat ini:
/dev/mapper/root  100G  42G   58G  43% /

Pola << EOF dibaca: "mulai menulis teks, lanjutkan sampai menemukan baris yang isinya persis EOF". Semua yang ada di antara delimiter dialirkan ke stdin dari perintah di depannya (cat). Teks bisa panjang berpuluh baris — ini pengganti menulis banyak echo yang tidak praktis dan rawan salah kutip.

Perhatikan pada contoh di atas: $(hostname) dan $(df -h / | tail -1) ikut diekspansi — karena delimiter EOF ditulis tanpa tanda kutip. Ini fitur, bukan bug: heredoc yang tidak dikutip adalah template yang hidup. Namun justru di sinilah letak jebakan tersembunyi — kalian tidak selalu ingin ekspansi terjadi.

Untuk menulis teks yang harus literal apa adanya — isi file konfigurasi yang punya $, backtick, atau sintaks template — kutip delimiter:

Here-document dengan delimiter dikutip (tanpa ekspansi)
cat << 'EOF'
Ini literal: $HOME, $(date), dan `pwd` tidak akan diekspansi.
Cocok untuk menulis template, konfigurasi, atau kode program.
EOF
Output
Ini literal: $HOME, $(date), dan `pwd` tidak akan diekspansi.
Cocok untuk menulis template, konfigurasi, atau kode program.

Aturan praktisnya: tanpa kutip = ekspansi; dengan kutip = literal. Dan ada varian ketiga untuk kebersihan indentasi: <<- EOF memungkinkan baris teks diberi awalan tab (bukan spasi) yang otomatis dibuang — berguna saat heredoc berada di dalam fungsi yang sudah terindentasi, menjaga kode tetap rapi tanpa merusak format teks.

Satu lagi bentuk yang serupa tapi berbeda: here-string <<<. Ia mengirim sebuah string (bukan blok teks) ke stdin, dan otomatis menambahkan baris baru di akhir. Ini adalah cara paling ringkas untuk memberi input ke perintah yang hanya mau membaca stdin:

Here-string
grep -o 'GET[^"]*' <<< "$(cat access.log)"
md5sum <<< "pesan rahasia"

Here-string sangat berguna untuk md5sum, sha256sum, grep, dan perintah lain yang tidak bisa menerima input lewat argumen posisi. Pola $(wc -l <<< "$files") yang kita pakai di awal episode juga memanfaatkannya — wc membaca dari stdin, dan here-string menyuntikkan string $files tanpa menulis file.

Praktik Nyata: Generate File Konfigurasi dari Dalam Skrip

Sekarang mari gabungkan semuanya dalam satu skenario production: skrip deployment yang harus membuat file konfigurasi Nginx untuk site baru, dengan nilai-nilai yang dinamis (nama domain, direktori root, port upstream). Perhatikan bagaimana heredoc yang tidak dikutip menjadi template dan heredoc terkutip menjaga bagian yang harus literal:

deploy-site.sh — generate konfigurasi Nginx
#!/bin/bash
 
DOMAIN="${1:?Domain wajib diberikan, mis. ./deploy-site.sh example.com}"
ROOT_DIR="/var/www/${DOMAIN}"
UPSTREAM_PORT=3000
 
mkdir -p "$ROOT_DIR"
 
cat > "/etc/nginx/sites-available/${DOMAIN}" << EOF
server {
    listen 80;
    server_name ${DOMAIN} www.${DOMAIN};
 
    root ${ROOT_DIR};
    index index.html;
 
    location / {
        proxy_pass http://127.0.0.1:${UPSTREAM_PORT};
        include /etc/nginx/proxy_params;
    }
}
EOF
 
echo "Konfigurasi untuk ${DOMAIN} berhasil dibuat di /etc/nginx/sites-available/"

Mari bedah hal-hal penting dalam skrip di atas:

  • cat > path << EOF — output heredoc ditulis ke file (bukan ke stdout) karena ada redirection >.
  • ${DOMAIN}, ${ROOT_DIR}, ${UPSTREAM_PORT} diekspansi karena delimiter tidak dikutip — inilah yang membuat satu template bisa melayani banyak site.
  • ${1:?pesan} (dari episode 16) memastikan user wajib memberikan argumen domain, kalau tidak skrip berhenti.
  • Bagian proxy_pass dan blok server ditulis dalam template — semua literal kecuali variabel yang kita sisipkan.

Jika kita punya bagian yang harus benar-benar literal — misalnya menulis script di dalam script, atau template yang memuat $ milik program lain — gunakan delimiter terkutip untuk bagian itu.

Tip

Kombinasi process substitution + heredoc + command substitution adalah trio yang paling sering dipakai di skrip deployment production: command substitution untuk mengambil nilai (IP server, tanggal, versi), heredoc untuk mencetak template konfigurasi yang bersih, dan process substitution untuk membandingkan atau menggabungkan stream tanpa file sementara. Kuasai ketiganya, dan skrip kalian akan terasa "dewasa" dalam semalam.

Jebakan Klasik (Common Pitfalls)

1. Lupa menulis delimiter penutup. BASH membaca heredoc sampai menemukan baris yang isinya persis delimiter. Jika kalian lupa menulis EOF di akhir, BASH akan terus "menunggu" hingga akhir file — dan kalian mendapatkan error unexpected end of file. Solusi: biasakan menulis penutup segera setelah pembuka, lalu isi bagian tengahnya.

2. Trailing whitespace pada baris penutup. Baris EOF dengan satu spasi di ujungnya (atau indentasi spasi pada <<- yang mengharuskan tab) tidak akan dikenali sebagai penutup. Ini adalah penyebab paling umum heredoc "macet". Periksa dengan cat -A bila perlu untuk melihat karakter tersembunyi.

3. Delimiter tidak dikutip → ekspansi tak terduga. Ini sumber bug favorit: kalian menulis template konfigurasi yang berisi $ (misalnya variabel environment aplikasi lain, atau sintaks $ANSI warna), lalu heredoc dengan polosnya mengekspansinya menjadi kosong. Kalau maksud kalian literal, kutip delimiter-nya (<< 'EOF'). Kalau maksud kalian template, kutip variabel yang ingin kalian ekspansi di dalam heredoc.

4. Salah menangkap perbedaan $(...) dan ${...}. $(cmd) menjalankan perintah; ${var} membaca variabel. Keduanya sering tertukar oleh penulis skrip yang baru pertama kali melihat keduanya. Ketika output variabel kosong secara misterius, tanya dulu: apakah kalian menulis $() padahal yang kalian maksud ${}?

5. Backtick yang lolos dari review. Skrip lama berisi `command` tetap berjalan — jadi bug-nya tidak terlihat. Namun saat di-nest atau dalam quoting yang rumit, backtick jadi sumber perilaku aneh. Jika kalian melihat backtick dalam skrip yang sedang kalian refactor, ganti dengan $(...) — ini salah satu perbaikan paling aman yang bisa dilakukan.

Caution

Command substitution menjalankan perintah di subshell — setiap $(...) memulai proses baru. Untuk lima atau sepuluh panggilan, biayanya sepele. Tetapi jika kalian memanggil $(...) di dalam perulangan yang berjalan ribuan kali (misalnya for f in *.log; do size=$(stat -c%s "$f"); ...), overhead pembuatan subshell terasa. Pertimbangkan menangkap hasil satu kali di luar loop, atau memakai proses yang benar-benar perlu di-substitute.

Penutup

Di episode 17 ini kalian telah belajar tiga cara skrip mengambil dan mengeluarkan data dari dunia nyata: command substitution $(...) untuk menangkap output perintah menjadi variabel (dan mengapa backtick sudah usang), process substitution <(cmd) dan >(cmd) untuk memperlakukan output sebagai file tanpa menyentuh disk, serta here-document << EOF dan here-string <<< untuk menulis teks panjang dan memberi input multi-baris dengan rapi.

Poin kunci yang harus kalian bawa pulang:

  • output=$(command) adalah standar modern; hindari backtick.
  • Ekspansi terjadi di heredoc hanya jika delimiter tidak dikutip; kutip delimiter untuk teks literal.
  • Process substitution menyajikan output sebagai file deskriptor — hemat langkah, nol file sementara.
  • Perhatikan trailing whitespace pada delimiter penutup dan bedakan selalu $() dengan ${}.
  • Command substitution berjalan di subshell — hindari panggilan massal di dalam loop ketat.

Sekarang kalian bisa mengambil data dari perintah lain dan mencetak artefak yang rapi. Namun ada satu jurang yang belum kita jembatani: apa yang terjadi ketika sesuatu gagal? Skrip yang berjalan mulus di laptop tiba-tiba mematahkan segalanya di server — karena perintah-perintahnya gagal dengan cara yang tidak pernah kita antisipasi. Di episode 18 selanjutnya kita akan membangun ketangguhan dengan topik Error Handling & Robustness (Unofficial BASH Strict Mode): memahami exit codes, set -euo pipefail, mengizinkan kegagalan yang memang disengaja, dan memperkuat skrip kalian lapis demi lapis sampai sulit untuk rusak diam-diam. Sampai jumpa!