Mesin baru terasa menakutkan bukan karena sulit dikonfigurasi, melainkan karena tidak ada yang mencatat apa saja yang dulu dipasang. Kita ingat compiler ada, lupa apakah versinya cukup; yakin git terpasang, tidak yakin alias lamanya ke mana. Pada bagian ini kita mengganti ingatan dengan skrip: satu daftar tool esensial, satu fungsi pemeriksa yang melaporkan ada-tidaknya dan versinya, dan satu skrip setup.sh yang tahu mode --check — pratinjau lengkap tanpa efek samping. Seluruhnya berjalan sebagai user biasa, dan karena pemeriksaan tool bersifat laporan — bukan instalasi paksa — skrip aman dijalankan di mesin apa pun.

Tool Esensial dan Cara Menanyakannya #

Lingkungan development di course ini berdiri di atas lima tool inti. Tabel berikut memetakan masing-masing beserta nama paket Debian dan perintah yang dipakai untuk menanyakan versinya:

ToolPaket DebianCek versiPeran
gccgcc / build-essentialgcc -dumpfullversioncompiler C dan C++
makemake / build-essentialmake --versionbuild automation lewat Makefile
gitgitgit --versionversion control
python3python3python3 --versionskrip, alat bantu, pengujian
curlcurlcurl --versionuji HTTP dan unduh berkas

Di Debian, paket meta build-essential memasang gcc, make, dan header pustaka sekaligus — pilihan praktis bila mesin benar-benar kosong. Perbedaan tiap anggota toolchain beserta cara shell menemukan mereka lewat PATH sudah kita bedah di Chapter Compilers dan Build Tools ; di sini kita cukup mengambil satu mekanisme dari chapter itu: command -v nama mengembalikan jalur bila tool ada, dan gagal bila tidak — persis yang kita butuhkan untuk pemeriksaan terprogram — perilakunya terdokumentasi di bash(1) .

Tool di luar daftar — Node.js, Go, Rust, Java — bersifat per jenis proyek. Kita tidak menjadikannya prasyarat; skrip kita dirancang agar daftar tool mudah diperluas, dan Bagian 3 nanti menyediakan template proyek yang siap bagi mesin yang memilikinya.

Tiga Fungsi Kecil: versi_cukup, versi_tool, cek_tool #

Pemeriksaan satu tool menjawab tiga pertanyaan berurutan: apakah terpasang, versi berapa, dan apakah versinya cukup. Kita tulis sebagai tiga fungsi kecil — blok berikut mendefinisikannya lalu memanggilnya untuk kelima tool inti ditambah satu contoh tool yang memang tidak ada di mesin ini:

bash
# versi_cukup <versi> <minimum>: keluar 0 bila versi >= minimum.
# Hanya major.minor yang dibandingkan — patch tidak menentukan layak/tidak.
versi_cukup() {
    awk -v v="$1" -v m="$2" 'BEGIN {
        split(v, a, "."); split(m, b, ".");
        if (a[1] + 0 < b[1] + 0) exit 1;
        if (a[1] + 0 == b[1] + 0 && a[2] + 0 < b[2] + 0) exit 1;
    }'
}

# versi_tool <nama>: cetak nomor versi untuk tool yang dikenal.
versi_tool() {
    case $1 in
        gcc)     gcc -dumpfullversion ;;
        make)    make --version | sed -n '1s/^GNU Make //p' ;;
        git)     git --version | sed -n 's/^git version //p' ;;
        python3) python3 --version | sed -n 's/^Python //p' ;;
        curl)    curl --version | awk 'NR==1 {print $2}' ;;
        *)       printf '0' ;;
    esac
}

# cek_tool <nama> <minimum>: laporan satu baris + usul instalasi bila hilang.
#   exit 0 siap, 1 hilang, 2 versi terlalu tua.
cek_tool() {
    nama=$1
    min=$2
    if ! jalur=$(command -v "$nama" 2>/dev/null); then
        printf '[HILANG] %-8s tidak ditemukan\n' "$nama"
        printf '         usul: sudo apt-get install %s\n' "$nama"
        return 1
    fi
    versi=$(versi_tool "$nama")
    if versi_cukup "$versi" "$min"; then
        printf '[OK]     %-8s versi %s (minimum %s) — %s\n' \
            "$nama" "$versi" "$min" "$jalur"
        return 0
    fi
    printf '[TUA]    %-8s versi %s di bawah minimum %s\n' \
        "$nama" "$versi" "$min"
    return 2
}

cek_tool gcc 10.0
cek_tool make 4.0
cek_tool git 2.30
cek_tool python3 3.8
cek_tool curl 7.70
cek_tool rustc 1.70; echo "exit=$?"

Output yang Dihasilkan:

text
[OK]     gcc      versi 14.2.0 (minimum 10.0) — /usr/bin/gcc
[OK]     make     versi 4.4.1 (minimum 4.0) — /usr/bin/make
[OK]     git      versi 2.47.3 (minimum 2.30) — /usr/bin/git
[OK]     python3  versi 3.13.5 (minimum 3.8) — /usr/bin/python3
[OK]     curl     versi 8.14.1 (minimum 7.70) — /usr/bin/curl
[HILANG] rustc    tidak ditemukan
         usul: sudo apt-get install rustc
exit=1

Mari kita bedah ketiganya satu per satu:

  1. versi_cukup: perbandingan versi dilakukan di awk dengan memecah string pada titik lalu membandingkan major dan minor sebagai angka (+ 0 memaksa konversi numerik, sehingga 4 dan 4.0 dianggap sama). Patch sengaja tidak dibandingkan — keputusan “layak dipakai” jarang bergantung pada nomor patch, dan logika dua tingkat lebih mudah dibuktikan benar.
  2. versi_tool: setiap tool mencetak versi dengan format berbeda — make menyisipkan kata “GNU Make”, git menyisipkan “git version” — jadi tiap tool dapat satu baris sed/awk yang memotong ke nomornya saja. Fungsi *) di akhir case mengembalikan 0 untuk nama yang tidak dikenal agar pemanggil tetap mendapat nilai, bukan string kosong.
  3. cek_tool: pola if ! jalur=$(command -v ...) menangkap hasil sekaligus keadaan gagal — command -v mengembalikan exit code bukan nol saat tool tidak ada, dan stderr yang tidak relevan dibuang ke /dev/null. Nomor keluar fungsinya dibuat berbeda per kondisi (0/1/2) agar pemanggil bisa membedakan “hilang” dari “tua” tanpa mem-parsing teks laporan.
  4. Pemanggilan cek_tool rustc 1.70; echo "exit=$?": rustc memang tidak terpasang di mesin ini, sehingga baris itu memperlihatkan jalur laporan [HILANG] lengkap dengan usul apt-get install — diikuti echo "exit=$?" yang membuktikan kode keluarnya 1. Perhatikan bahwa laporan usul berupa teks; skrip tidak menjalankan sudo apa pun.

setup.sh Tahap 1 dengan Mode –check #

Ketiga fungsi itu kini kita bungkus menjadi setup.sh versi tahap 1: membaca daftar tool beserta versi minimum, melaporkan semuanya, dan menghitung ringkasan. Mode --check mengubah seluruh perilaku menjadi dry-run — laporan saja, tidak mengubah apa pun. Blok berikut menulis skripnya lalu menjalankannya dua kali di mesin ini, lengkap dengan bukti bahwa laporan identik dan home tidak tersentuh:

bash
mkdir -p dev-env/scripts
cat > dev-env/scripts/setup.sh <<'BASH'
#!/usr/bin/env bash
# setup.sh — tahap 1: periksa tool development.
#
# Pemakaian:
#   setup.sh            periksa tool (tahap 2: dotfiles, menyusul)
#   setup.sh --check    dry-run: laporan saja, tidak mengubah apa pun
#
# Kode keluar:
#   0 sukses                     1 opsi tidak dikenal
#   3 tool inti hilang/terlalu tua

set -u

MODE=pasang
for arg in "$@"; do
    case $arg in
        --check) MODE=cek ;;
        *)
            printf 'setup.sh: opsi tidak dikenal: %s\n' "$arg" >&2
            printf 'pemakaian: setup.sh [--check]\n' >&2
            exit 1
            ;;
    esac
done

if [ "$MODE" = cek ]; then
    printf 'mode --check: laporan saja, tidak ada yang diubah\n'
fi

# --- tool inti: nama:versi-minimum ------------------------------------
DAFTAR_TOOL="gcc:10.0 make:4.0 git:2.30 python3:3.8 curl:7.70"

versi_cukup() {
    awk -v v="$1" -v m="$2" 'BEGIN {
        split(v, a, "."); split(m, b, ".");
        if (a[1] + 0 < b[1] + 0) exit 1;
        if (a[1] + 0 == b[1] + 0 && a[2] + 0 < b[2] + 0) exit 1;
    }'
}

versi_tool() {
    case $1 in
        gcc)     gcc -dumpfullversion ;;
        make)    make --version | sed -n '1s/^GNU Make //p' ;;
        git)     git --version | sed -n 's/^git version //p' ;;
        python3) python3 --version | sed -n 's/^Python //p' ;;
        curl)    curl --version | awk 'NR==1 {print $2}' ;;
        *)       printf '0' ;;
    esac
}

cek_tool() {
    nama=$1
    min=$2
    if ! jalur=$(command -v "$nama" 2>/dev/null); then
        printf '[HILANG] %-8s tidak ditemukan\n' "$nama"
        printf '         usul: sudo apt-get install %s\n' "$nama"
        return 1
    fi
    versi=$(versi_tool "$nama")
    if versi_cukup "$versi" "$min"; then
        printf '[OK]     %-8s versi %s (minimum %s) — %s\n' \
            "$nama" "$versi" "$min" "$jalur"
        return 0
    fi
    printf '[TUA]    %-8s versi %s di bawah minimum %s\n' \
        "$nama" "$versi" "$min"
    return 2
}

printf '== tahap 1: tool development ==\n'
hilang=0
# shellcheck disable=SC2086  # daftar tool memang dipisah spasi
for item in $DAFTAR_TOOL; do
    nama=${item%%:*}
    min=${item#*:}
    cek_tool "$nama" "$min" || hilang=$((hilang + 1))
done
# shellcheck disable=SC2086
printf 'ringkasan: %d tool diperiksa, %d bermasalah\n' \
    "$(printf '%s\n' $DAFTAR_TOOL | wc -w)" "$hilang"

if [ "$hilang" -gt 0 ]; then
    printf 'setup.sh: selesai dengan catatan: %d tool bermasalah\n' "$hilang"
    exit 3
fi
printf 'setup.sh: selesai — lingkungan siap\n'
exit 0
BASH

echo "== eksekusi pertama: --check =="
bash dev-env/scripts/setup.sh --check; echo "exit=$?"
echo "== eksekusi kedua: laporan identik, home tidak tersentuh =="
bash dev-env/scripts/setup.sh --check > laporan-1.txt
bash dev-env/scripts/setup.sh --check > laporan-2.txt
diff laporan-1.txt laporan-2.txt && echo "laporan identik antar eksekusi"
n=$(find "$HOME" -maxdepth 1 -name '.[a-z]*' | wc -l)
echo "dotfiles di home: $n"

Output yang Dihasilkan:

text
== eksekusi pertama: --check ==
mode --check: laporan saja, tidak ada yang diubah
== tahap 1: tool development ==
[OK]     gcc      versi 14.2.0 (minimum 10.0) — /usr/bin/gcc
[OK]     make     versi 4.4.1 (minimum 4.0) — /usr/bin/make
[OK]     git      versi 2.47.3 (minimum 2.30) — /usr/bin/git
[OK]     python3  versi 3.13.5 (minimum 3.8) — /usr/bin/python3
[OK]     curl     versi 8.14.1 (minimum 7.70) — /usr/bin/curl
ringkasan: 5 tool diperiksa, 0 bermasalah
setup.sh: selesai — lingkungan siap
exit=0
== eksekusi kedua: laporan identik, home tidak tersentuh ==
laporan identik antar eksekusi
dotfiles di home: 0

Bagian-bagian kunci skripnya:

  1. MODE=pasang dan loop for arg in "$@": satu-satunya opsi saat ini --check; opsi lain ditolak ke stderr dengan pesan pemakaian dan kode keluar 1 — salah ketik --Check harus terdengar saat itu juga, bukan diam-diam diabaikan.
  2. DAFTAR_TOOL="gcc:10.0 make:4.0 ...": seluruh kebijakan tool hidup dalam satu string nama:minimum. Menambah tool baru berarti menambah satu kata — loop for item in $DAFTAR_TOOL memecahnya dengan ${item%%:*} (nama) dan ${item#*:} (minimum), dua pola parameter expansion yang sudah kita kenal.
  3. cek_tool "$nama" "$min" || hilang=$((hilang + 1)): laporan dicetak ke stdout, sedangkan kegagalan dihitung lewat operator || — pemisahan ini membuat ringkasan akhir bisa dihitung tanpa mem-parsing teks laporan sendiri.
  4. exit 3 saat ada tool bermasalah: skrip tetap menyelesaikan seluruh laporan sebelum keluar, lalu memakai kode yang berbeda dari 0/1/2 agar pemanggilnya — manusia, cron, atau test suite — tahu bahwa ada tool yang perlu ditindaklanjuti.
  5. find "$HOME" -maxdepth 1 -name '.[a-z]*' | wc -l di akhir blok: bukti numerik bahwa --check tidak meninggalkan satu dotfile pun di home. Dikombinasikan dengan diff antara dua laporan, keduanya adalah janji dry-run yang diperiksa, bukan sekadar dijanjikan.

Perhatikan pula bahwa skrip ini idempoten sejak lahir: seluruh pekerjaannya membaca keadaan sistem (command -v, --version) tanpa menulis apa pun. Sifat itu akan tertantang di Bagian 2, saat setup.sh mulai memasang dotfile — di sanalah idempotensi harus dirancang, bukan diasumsikan.

Instalasi via apt: Bagian yang Butuh Root #

Saat cek_tool melaporkan [HILANG], usulan perbaikannya adalah perintah apt-get seperti berikut:

bash
sudo apt-get update
sudo apt-get install -y build-essential git python3 curl

Skrip kita sengaja tidak menjalankan keduanya sendiri. apt-get install membutuhkan root, sedangkan lingkungan latihan dan sebagian besar kebiasaan kerja kita berjalan sebagai user biasa; menggabungkan keduanya dalam satu skrip berarti skrip gagal total di mesin tanpa kunci sudo. Skrip memilih jalur yang lebih tahan banting: laporkan yang hilang, usulkan perintahnya, lalu lanjut — pemilik mesin yang memutuskan kapan menjalankan bagian root. Setelah instalasi selesai, menjalankan setup.sh lagi akan memastikan kelima baris berubah menjadi [OK].

Dengan laporan tool yang bekerja dan terbukti tidak menyentuh apa pun, fondasi proyek ini selesai. Kekurangannya tinggal satu: lingkungan development bukan hanya tool, melainkan juga kebiasaan — alias, prompt, identitas git. Bagian 2 melengkapinya lewat dotfiles yang terkelola dan pemasangan yang aman diulang.

Berkas Sumber

Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.

scripts/proyek-baru.sh

Unduh berkas
bash
#!/usr/bin/env bash
# proyek-baru.sh — scaffold proyek baru siap kerja dalam satu perintah.
#
# Pemakaian:
#   proyek-baru.sh <nama> [jenis]
#     nama  : nama folder proyek, kebab-case disarankan
#     jenis : c | python | node   (default: c)
#
# Membuat folder berisi repo git (branch main, satu commit awal),
# README.md, .gitignore, Makefile, dan src/ dengan berkas awal sesuai
# jenis. Tidak pernah menimpa: nama yang sudah dipakai ditolak.
#
# Variabel lingkungan:
#   DEV_SRC   folder config berisi template (default: <skrip>/../config)
#   GIT_AUTHOR_NAME / GIT_AUTHOR_EMAIL — identitas commit bila
#   konfigurasi global git belum diisi (default: Pengembang)
#
# Kode keluar:
#   0 sukses                        1 pemakaian salah
#   2 nama sudah dipakai / template tidak ada

set -u

BASE=$(cd "$(dirname "$0")" && pwd)
DEV_SRC="${DEV_SRC:-$BASE/../config}"

gagal() {
    printf 'proyek-baru.sh: %s\n' "$1" >&2
    exit "$2"
}

if [ $# -lt 1 ] || [ $# -gt 2 ]; then
    gagal 'pemakaian: proyek-baru.sh <nama> [c|python|node]' 1
fi

NAMA=$1
JENIS=${2:-c}

case $JENIS in
    c|python|node) ;;
    *) gagal "jenis tidak dikenal: $JENIS (pilih c, python, atau node)" 1 ;;
esac

case $NAMA in
    ''|-*) gagal "nama tidak valid: '$NAMA' (jangan kosong atau diawali -)" 1 ;;
    */*)  gagal "nama tidak boleh memuat '/'" 1 ;;
esac

[ -e "$NAMA" ] && gagal "sudah ada: $NAMA — pilih nama lain" 2

template="$DEV_SRC/gitignore-$JENIS"
[ -f "$template" ] || gagal "template tidak ditemukan: $template" 2

mkdir -p "$NAMA/src"
cd "$NAMA" || exit 1

git init -q -b main

# --- berkas dasar ----------------------------------------------------------
cp "$template" .gitignore
if [ "$JENIS" = c ]; then
    # Nama binary hasil build mengikuti nama proyek — abaikan lewat pola
    # yang dijangkar ke akar repo (/<nama>) agar hanya binary di root yang
    # tidak dilacak git.
    printf '# binary hasil build make\n/%s\n' "$NAMA" >> .gitignore
fi

printf '# %s\n\nProyek %s (jenis: %s) dibuat oleh proyek-baru.sh.\n\n## Menjalankan\n\n    make run\n' \
    "$NAMA" "$NAMA" "$JENIS" > README.md

# --- Makefile dan berkas awal sesuai jenis -----------------------------------
if [ "$JENIS" = c ]; then
    cat > Makefile <<MAKE
# Makefile — tugas build untuk proyek C.
BIN = $NAMA
CC = gcc
CFLAGS = -Wall -Wextra -std=c17

\$(BIN): src/main.c
	\$(CC) \$(CFLAGS) -o \$(BIN) src/main.c

run: \$(BIN)
	./\$(BIN)

clean:
	rm -f \$(BIN)

.PHONY: run clean
MAKE
    cat > src/main.c <<'C'
/* main.c — titik masuk proyek. */
#include <stdio.h>

int main(void)
{
    printf("halo, dunia\n");
    return 0;
}
C
elif [ "$JENIS" = python ]; then
    cat > Makefile <<'MAKE'
# Makefile — tugas umum proyek Python.
PY = python3

run:
	$(PY) src/main.py

.PHONY: run
MAKE
    cat > src/main.py <<'PY'
"""main.py — titik masuk proyek."""


def main() -> None:
    print("halo, dunia")


if __name__ == "__main__":
    main()
PY
else
    cat > Makefile <<'MAKE'
# Makefile — tugas umum proyek Node.js.
NODE = node

run:
	$(NODE) src/index.js

.PHONY: run
MAKE
    cat > src/index.js <<'JS'
// index.js — titik masuk proyek.
console.log("halo, dunia");
JS
fi

# --- identitas commit --------------------------------------------------------
# Pakai konfigurasi git bila sudah ada; jika belum, nilai bawaan yang
# bisa di-override lewat environment (berguna untuk pengujian).
if [ -z "$(git config --get user.name 2>/dev/null)" ]; then
    : "${GIT_AUTHOR_NAME:=Pengembang}"
    : "${GIT_AUTHOR_EMAIL:[email protected]}"
    export GIT_AUTHOR_NAME GIT_AUTHOR_EMAIL
    export GIT_COMMITTER_NAME="$GIT_AUTHOR_NAME"
    export GIT_COMMITTER_EMAIL="$GIT_AUTHOR_EMAIL"
fi

git add -A
git commit -q -m "commit awal: kerangka $JENIS"

printf 'proyek siap: %s (jenis %s)\n' "$NAMA" "$JENIS"
printf '  cd %s && make run\n' "$NAMA"
exit 0

scripts/setup.sh

Unduh berkas
bash
#!/usr/bin/env bash
# setup.sh — bangun lingkungan development dalam satu perintah.
#
# Memeriksa toolchain inti (gcc, make, git, python3, curl), memasang
# dotfiles dari config/ dengan backup .bak untuk yang lama, lalu
# melaporkan hasilnya. Idempoten: dijalankan berapa kali pun dengan isi
# config/ yang sama, keadaan akhirnya selalu sama.
#
# Pemakaian:
#   setup.sh            periksa tool lalu pasang dotfiles
#   setup.sh --check    dry-run: laporan saja, tidak mengubah apa pun
#
# Variabel lingkungan (berguna untuk pengujian):
#   DEV_HOME  folder home tujuan pemasangan (default: $HOME)
#   DEV_SRC   folder config berisi dotfile  (default: <skrip>/../config)
#
# Kode keluar:
#   0 sukses                        1 opsi tidak dikenal
#   2 config/ tidak ditemukan       3 tool inti hilang/terlalu tua

set -u

MODE=pasang
for arg in "$@"; do
    case $arg in
        --check) MODE=cek ;;
        *)
            printf 'setup.sh: opsi tidak dikenal: %s\n' "$arg" >&2
            printf 'pemakaian: setup.sh [--check]\n' >&2
            exit 1
            ;;
    esac
done

BASE=$(cd "$(dirname "$0")" && pwd)
DEV_SRC="${DEV_SRC:-$BASE/../config}"
DEV_HOME="${DEV_HOME:-$HOME}"

if [ ! -d "$DEV_SRC" ]; then
    printf 'setup.sh: folder config tidak ditemukan: %s\n' "$DEV_SRC" >&2
    exit 2
fi

if [ "$MODE" = cek ]; then
    printf 'mode --check: laporan saja, tidak ada yang diubah\n'
fi

# --- tool inti: nama:versi-minimum ---------------------------------------
DAFTAR_TOOL="gcc:10.0 make:4.0 git:2.30 python3:3.8 curl:7.70"

# versi_cukup <versi> <minimum>: keluar 0 bila versi >= minimum.
# Hanya major.minor yang dibandingkan — patch tidak menentukan layak/tidak.
versi_cukup() {
    awk -v v="$1" -v m="$2" 'BEGIN {
        split(v, a, "."); split(m, b, ".");
        if (a[1] + 0 < b[1] + 0) exit 1;
        if (a[1] + 0 == b[1] + 0 && a[2] + 0 < b[2] + 0) exit 1;
    }'
}

# versi_tool <nama>: cetak nomor versi untuk tool yang dikenal.
versi_tool() {
    case $1 in
        gcc)     gcc -dumpfullversion ;;
        make)    make --version | sed -n '1s/^GNU Make //p' ;;
        git)     git --version | sed -n 's/^git version //p' ;;
        python3) python3 --version | sed -n 's/^Python //p' ;;
        curl)    curl --version | awk 'NR==1 {print $2}' ;;
        *)       printf '0' ;;
    esac
}

# cek_tool <nama> <minimum>: laporan satu baris + usul instalasi bila hilang.
#   exit 0 siap, 1 hilang, 2 versi terlalu tua.
cek_tool() {
    nama=$1
    min=$2
    if ! jalur=$(command -v "$nama" 2>/dev/null); then
        printf '[HILANG] %-8s tidak ditemukan\n' "$nama"
        printf '         usul: sudo apt-get install %s\n' "$nama"
        return 1
    fi
    versi=$(versi_tool "$nama")
    if versi_cukup "$versi" "$min"; then
        printf '[OK]     %-8s versi %s (minimum %s) — %s\n' \
            "$nama" "$versi" "$min" "$jalur"
        return 0
    fi
    printf '[TUA]    %-8s versi %s di bawah minimum %s\n' \
        "$nama" "$versi" "$min"
    return 2
}

# --- tahap 1: periksa tool ------------------------------------------------
printf '== tahap 1: tool development ==\n'
hilang=0
# shellcheck disable=SC2086  # daftar tool memang dipisah spasi
for item in $DAFTAR_TOOL; do
    nama=${item%%:*}
    min=${item#*:}
    cek_tool "$nama" "$min" || hilang=$((hilang + 1))
done
# shellcheck disable=SC2086
printf 'ringkasan: %d tool diperiksa, %d bermasalah\n' \
    "$(printf '%s\n' $DAFTAR_TOOL | wc -w)" "$hilang"

# --- tahap 2: pasang dotfiles ----------------------------------------------
# pasang_dotfile <sumber> <target>: salin dengan backup; idempoten.
pasang_dotfile() {
    sumber=$1
    target=$2
    if [ -f "$target" ] && cmp -s "$sumber" "$target"; then
        printf '[OK]     %s sudah terpasang\n' "$target"
        return 0
    fi
    if [ "$MODE" = cek ]; then
        printf '[BARU]   %s akan dipasang\n' "$target"
        return 0
    fi
    if [ -f "$target" ]; then
        cp "$target" "$target.bak"
        printf '[BAK]    %s -> %s.bak\n' "$target" "$target"
    fi
    cp "$sumber" "$target"
    printf '[PASANG] %s\n' "$target"
}

# pasang_hook_bashrc: pastikan ~/.bashrc memuat ~/.bashrc-dev (sekali saja).
pasang_hook_bashrc() {
    bashrc="$DEV_HOME/.bashrc"
    if [ -f "$bashrc" ] && grep -qF '.bashrc-dev' "$bashrc"; then
        printf '[OK]     %s sudah memuat .bashrc-dev\n' "$bashrc"
        return 0
    fi
    if [ "$MODE" = cek ]; then
        printf '[BARU]   hook .bashrc akan memuat .bashrc-dev\n'
        return 0
    fi
    if [ -f "$bashrc" ]; then
        cp "$bashrc" "$bashrc.bak"
        printf '[BAK]    %s -> %s.bak\n' "$bashrc" "$bashrc"
    fi
    {
        printf '\n'
        printf '# >>> lingkungan-dev: muat kustomisasi shell >>>\n'
        printf '[ -f "$HOME/.bashrc-dev" ] && . "$HOME/.bashrc-dev"\n'
        printf '# <<< lingkungan-dev <<<\n'
    } >> "$bashrc"
    printf '[PASANG] hook .bashrc memuat .bashrc-dev\n'
}

printf '== tahap 2: dotfiles ==\n'
pasang_dotfile "$DEV_SRC/.bashrc-dev"   "$DEV_HOME/.bashrc-dev"
pasang_dotfile "$DEV_SRC/.gitconfig"   "$DEV_HOME/.gitconfig"
pasang_dotfile "$DEV_SRC/.editorconfig" "$DEV_HOME/.editorconfig"
pasang_hook_bashrc

if [ "$hilang" -gt 0 ]; then
    printf 'setup.sh: selesai dengan catatan: %d tool bermasalah\n' "$hilang"
    exit 3
fi
printf 'setup.sh: selesai — lingkungan siap\n'
exit 0

tests/run-tests.sh

Unduh berkas
bash
#!/usr/bin/env bash
# run-tests.sh — test suite untuk paket lingkungan-development-linux.
#
# Suite menyiapkan folder kerja terisolasi (mktemp), menyalin skrip
# dari scripts/ dan dotfile dari config/, lalu menjalankan rangkaian
# pengujian dengan assertion sederhana:
#   - setup.sh --check melaporkan tanpa mengubah apa pun,
#   - setup.sh idempoten: dua eksekusi, keadaan akhir identik,
#   - dotfiles terpasang dengan isi identik + hook .bashrc sekali saja,
#   - backup .bak menyimpan versi lama sebelum ditimpa,
#   - tool inti hilang dilaporkan dengan keluar 3 (PATH tiruan),
#   - proyek-baru.sh membuat struktur + repo git dengan commit awal,
#   - proyek hasil scaffold bisa dibangun dan dijalankan (make run),
#   - penolakan: nama terpakai, tanpa nama, jenis tak dikenal.
#
# Jalankan dari mana saja: bash tests/run-tests.sh

set -u

SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd)
PROJ_DIR=$(dirname "$SCRIPT_DIR")

PASS=0
FAIL=0

# assert_eq <harapan> <nyata> <pesan>: bandingkan dua nilai.
assert_eq() {
    if [ "$1" = "$2" ]; then
        PASS=$((PASS + 1))
        printf 'OK    %s\n' "$3"
    else
        FAIL=$((FAIL + 1))
        printf 'GAGAL %s: harapan %s, nyata %s\n' "$3" "$1" "$2"
    fi
}

# --- folder kerja terisolasi + salin paket ----------------------------------
WORK=$(mktemp -d)
mkdir -p "$WORK/paket/scripts" "$WORK/paket/config"
cp "$PROJ_DIR/scripts/setup.sh" "$PROJ_DIR/scripts/proyek-baru.sh" \
    "$WORK/paket/scripts/"
cp -R "$PROJ_DIR/config/." "$WORK/paket/config/"
SETUP="$WORK/paket/scripts/setup.sh"
BARU="$WORK/paket/scripts/proyek-baru.sh"

RUMAH="$WORK/rumah"
mkdir -p "$RUMAH"

# potret: checksum seluruh berkas di $RUMAH — bahan uji idempotensi.
potret() {
    (cd "$RUMAH" && find . -type f -print | sort | xargs sha256sum)
}

# --- uji 1: --check melaporkan tanpa mengubah ------------------------------
DEV_HOME="$RUMAH" bash "$SETUP" --check > "$WORK/cek1.txt"
assert_eq 0 "$?" "setup --check keluar 0"
grep -q '^mode --check' "$WORK/cek1.txt"
assert_eq 0 "$?" "mode --check diumumkan di laporan"
grep -q '^\[OK\]     gcc' "$WORK/cek1.txt"
assert_eq 0 "$?" "gcc dilaporkan OK beserta versinya"
n_baru=$(grep -c '^\[BARU\]' "$WORK/cek1.txt")
assert_eq 4 "$n_baru" "--check melaporkan 4 item akan dipasang"
n_berkas=$(find "$RUMAH" -type f | wc -l)
assert_eq 0 "$n_berkas" "--check tidak membuat berkas apa pun"

# --- uji 2: pemasangan pertama ----------------------------------------------
DEV_HOME="$RUMAH" bash "$SETUP" > "$WORK/pasang1.txt"
assert_eq 0 "$?" "setup pertama keluar 0"
[ -f "$RUMAH/.gitconfig" ]
assert_eq 0 "$?" "gitconfig terpasang"
cmp -s "$WORK/paket/config/.gitconfig" "$RUMAH/.gitconfig"
assert_eq 0 "$?" "isi gitconfig identik dengan template"
[ -f "$RUMAH/.editorconfig" ]
assert_eq 0 "$?" "editorconfig terpasang"
[ -f "$RUMAH/.bashrc-dev" ]
assert_eq 0 "$?" "bashrc-dev terpasang"
grep -qF '. "$HOME/.bashrc-dev"' "$RUMAH/.bashrc"
assert_eq 0 "$?" "hook .bashrc memuat bashrc-dev"
n=$(grep -c '^\[PASANG\]' "$WORK/pasang1.txt")
assert_eq 4 "$n" "empat item terpasang pada eksekusi pertama"

# --- uji 3: idempoten — eksekusi ulang tidak mengubah keadaan -----------------
potret > "$WORK/state1.txt"
DEV_HOME="$RUMAH" bash "$SETUP" > "$WORK/pasang2.txt"
assert_eq 0 "$?" "setup kedua keluar 0"
potret > "$WORK/state2.txt"
diff "$WORK/state1.txt" "$WORK/state2.txt" > /dev/null 2>&1
assert_eq 0 "$?" "keadaan identik setelah eksekusi kedua"
n=$(grep -c 'sudah terpasang' "$WORK/pasang2.txt")
assert_eq 3 "$n" "eksekusi kedua melaporkan 3 dotfile sudah terpasang"
grep -q 'sudah memuat .bashrc-dev' "$WORK/pasang2.txt"
assert_eq 0 "$?" "hook .bashrc tidak ditulis dua kali"

# --- uji 4: --check pada keadaan terpasang tetap tidak mengubah ----------------
potret > "$WORK/state3.txt"
DEV_HOME="$RUMAH" bash "$SETUP" --check > "$WORK/cek2.txt"
assert_eq 0 "$?" "setup --check (sudah terpasang) keluar 0"
potret > "$WORK/state4.txt"
diff "$WORK/state3.txt" "$WORK/state4.txt" > /dev/null 2>&1
assert_eq 0 "$?" "--check tidak mengubah keadaan terpasang"
n=$(grep -c 'sudah terpasang' "$WORK/cek2.txt")
assert_eq 3 "$n" "--check melaporkan 3 dotfile sudah terpasang"

# --- uji 5: backup .bak menyimpan versi lama -----------------------------------
printf '[user]\n\tname = Budi Lama\n' > "$RUMAH/.gitconfig"
DEV_HOME="$RUMAH" bash "$SETUP" > "$WORK/pasang3.txt"
assert_eq 0 "$?" "setup ulang setelah perubahan keluar 0"
grep -q '^\[BAK\]' "$WORK/pasang3.txt"
assert_eq 0 "$?" "backup .bak dibuat sebelum menimpa"
grep -q 'name = Budi Lama' "$RUMAH/.gitconfig.bak"
assert_eq 0 "$?" "isi lama tersimpan di .gitconfig.bak"
cmp -s "$WORK/paket/config/.gitconfig" "$RUMAH/.gitconfig"
assert_eq 0 "$?" "gitconfig kembali identik dengan template"

# --- uji 6: tool inti hilang dilaporkan keluar 3 --------------------------------
# PATH tiruan tanpa gcc: skrip harus melaporkan [HILANG] lalu keluar 3.
FAKEBIN="$WORK/fakebin"
mkdir -p "$FAKEBIN"
for t in bash awk sed grep wc cmp cp dirname make git python3 curl; do
    ln -s "$(command -v "$t")" "$FAKEBIN/$t"
done
PATH="$FAKEBIN" DEV_HOME="$WORK/rumah-fake" bash "$SETUP" --check \
    > "$WORK/cek3.txt"
assert_eq 3 "$?" "tool inti hilang dilaporkan keluar 3"
grep -q '^\[HILANG\] gcc' "$WORK/cek3.txt"
assert_eq 0 "$?" "gcc hilang terlihat di laporan"

# --- uji 7: opsi salah ditolak ---------------------------------------------------
bash "$SETUP" --salah > /dev/null 2>&1
assert_eq 1 "$?" "opsi tidak dikenal keluar 1"

# --- uji 8: proyek-baru membuat struktur + repo git -------------------------------
cd "$WORK" || exit 1
GIT_AUTHOR_NAME="Budi Santoso" GIT_AUTHOR_EMAIL="[email protected]" \
GIT_COMMITTER_NAME="Budi Santoso" GIT_COMMITTER_EMAIL="[email protected]" \
GIT_AUTHOR_DATE="2026-01-15 09:00:00 +0000" \
GIT_COMMITTER_DATE="2026-01-15 09:00:00 +0000" \
    bash "$BARU" aplikasi-tes c > "$WORK/baru1.txt"
assert_eq 0 "$?" "proyek-baru (c) keluar 0"
for f in README.md .gitignore Makefile src/main.c; do
    [ -f "aplikasi-tes/$f" ]
    assert_eq 0 "$?" "$f dibuat"
done
[ -d aplikasi-tes/.git ]
assert_eq 0 "$?" "repo git dibuat"
cabang=$(git -C aplikasi-tes symbolic-ref --short HEAD)
assert_eq main "$cabang" "branch utama bernama main"
n_commit=$(git -C aplikasi-tes log --oneline | wc -l)
assert_eq 1 "$n_commit" "satu commit awal dibuat"
n_kotor=$(git -C aplikasi-tes status --porcelain | wc -l)
assert_eq 0 "$n_kotor" "git status bersih setelah commit awal"

# --- uji 9: proyek hasil scaffold benar-benar bisa dijalankan ----------------------
make -s -C aplikasi-tes run > "$WORK/run-c.txt"
assert_eq 0 "$?" "make run (c) keluar 0"
grep -q 'halo, dunia' "$WORK/run-c.txt"
assert_eq 0 "$?" "program c menyapa halo, dunia"
n_kotor=$(git -C aplikasi-tes status --porcelain | wc -l)
assert_eq 0 "$n_kotor" "binary hasil build diabaikan .gitignore"

GIT_AUTHOR_NAME="Budi Santoso" GIT_AUTHOR_EMAIL="[email protected]" \
GIT_COMMITTER_NAME="Budi Santoso" GIT_COMMITTER_EMAIL="[email protected]" \
GIT_AUTHOR_DATE="2026-01-15 09:00:00 +0000" \
GIT_COMMITTER_DATE="2026-01-15 09:00:00 +0000" \
    bash "$BARU" web-tes python > "$WORK/baru2.txt"
assert_eq 0 "$?" "proyek-baru (python) keluar 0"
[ -f web-tes/src/main.py ]
assert_eq 0 "$?" "main.py dibuat"
make -s -C web-tes run > "$WORK/run-py.txt"
assert_eq 0 "$?" "make run (python) keluar 0"
grep -q 'halo, dunia' "$WORK/run-py.txt"
assert_eq 0 "$?" "program python menyapa halo, dunia"

# --- uji 10: penolakan --------------------------------------------------------------
bash "$BARU" aplikasi-tes c > /dev/null 2>&1
assert_eq 2 "$?" "nama yang sudah dipakai ditolak keluar 2"
bash "$BARU" > /dev/null 2>&1
assert_eq 1 "$?" "tanpa nama ditolak keluar 1"
bash "$BARU" tes-zig zig > /dev/null 2>&1
assert_eq 1 "$?" "jenis tidak dikenal ditolak keluar 1"

# --- ringkasan ------------------------------------------------------------------------
if [ "$FAIL" -eq 0 ]; then
    printf 'PASSED: %d/%d test lulus\n' "$PASS" "$PASS"
else
    printf 'FAILED: %d/%d test lulus\n' "$PASS" $((PASS + FAIL))
fi
cd / && rm -rf "$WORK"
[ "$FAIL" -eq 0 ]