Tool yang terpasang hanyalah setengah lingkungan development; setengahnya lagi adalah kebiasaan yang menempel pada user: alias yang membuat perintah panjang jadi pendek, prompt yang selalu tahu branch aktif, identitas git yang tidak boleh lupa diisi, dan aturan format yang sama di semua editor. Di mesin lama semua itu hidup diam-diam di ~/.bashrc dan ~/.gitconfig — dan lenyap bersama disk. Pada bagian ini kita memindahkan kebiasaan itu ke tiga dotfile template di folder proyek, lalu memperluas setup.sh agar memasangnya ke home dengan backup otomatis. Hasil akhirnya: konfigurasi ikut dikelola git, dan mesin baru cukup satu perintah untuk mendapatkannya kembali.

Tiga Dotfile Inti #

Kita bagi konfigurasi menjadi tiga berkas dengan peran tegas. .bashrc-dev memuat kustomisasi shell, .gitconfig mengatur identitas dan alias git, dan .editorconfig menyepakati format berkas lintas editor. Ketiganya tidak menimpa konfigurasi bawaan sistem — .bashrc-dev bahkan tidak menyentuh .bashrc Anda, melainkan dimuat darinya lewat hook yang dipasang setup.

.bashrc-dev: Kustomisasi Shell #

bash
# .bashrc-dev — kustomisasi shell untuk development.
# Berkas ini dipasang setup.sh sebagai ~/.bashrc-dev dan dimuat dari
# ~/.bashrc lewat hook yang dipasang di akhir berkas itu. Sunting versi
# di folder dotfiles lalu jalankan setup.sh lagi — jangan sunting yang
# di home secara langsung agar perubahan ikut tercatat oleh git.

# --- PATH: alat milik user --------------------------------------------
# pip --user memasang ke ~/.local/bin; alat bahasa lain (~/go/bin,
# ~/.cargo/bin) mengikuti pola sama — cukup tambahkan barisnya di sini.
[ -d "$HOME/.local/bin" ] && PATH="$HOME/.local/bin:$PATH"

# --- alias harian -------------------------------------------------------
alias ll='ls -alF'

# --- fungsi workflow ------------------------------------------------------
# mkcd: buat folder lalu langsung masuk ke dalamnya.
mkcd() {
    mkdir -p -- "$1" && cd -- "$1"
}

# git_branch: nama branch aktif; kosong bila bukan repo — untuk PS1.
git_branch() {
    local cabang
    cabang=$(git symbolic-ref --short HEAD 2>/dev/null) || return 0
    printf '(%s) ' "$cabang"
}

# --- PS1: prompt ringkas dengan branch git --------------------------------
PS1='\u@\h:\W$(git_branch)\$ '

Bedah singkat per bagian:

  1. Baris [ -d "$HOME/.local/bin" ] && PATH=...: folder ~/.local/bin adalah lokasi standar alat milik user — hasil pip install --user, skrip sendiri, dan sejenisnya. Prefiks [ -d ... ] && memastikan baris hanya menambah PATH bila foldernya ada, sehingga berkas ini aman dimuat di mesin mana pun tanpa menaruh folder hantu di PATH.
  2. Baris alias ll='ls -alF': satu alias pendamping yang paling sering dipakai — daftar panjang dengan penanda tipe. Menambah alias lain cukup mengikuti pola baris ini; git justru lebih baik di-alias di sisi git sendiri (.gitconfig), seperti yang kita lakukan di bawah.
  3. Fungsi mkcd: mkdir -p membuat folder bertingkat sekali jalan, cd langsung masuk — dua langkah yang hampir selalu berurutan. Tanda -- di kedua perintah menjaga nama folder yang diawali tanda minus agar tidak ditafsirkan sebagai opsi. Fungsi shell seperti ini dibahas tuntas di Chapter Functions .
  4. Fungsi git_branch: mengembalikan nama branch aktif atau diam di luar repo (|| return 0 menelan kegagalan symbolic-ref). Kata kunci local menjaga variabelnya tidak bocor ke shell pemanggil.
  5. Baris PS1='\u@\h:\W$(git_branch)\$ ': prompt yang menampilkan user, host, folder terakhir, lalu branch git dalam tanda kurung — dan hanya itu, berkas diberi tanda kutip tunggal agar $(git_branch) baru dievaluasi setiap kali prompt digambar, bukan sekali saat shell mulai.

.gitconfig: Identitas dan Alias Git #

ini
# .gitconfig — identitas dan kebiasaan git untuk development.
# Ganti name dan email dengan identitas Anda sebelum setup.sh
# memasangnya, atau sunting ~/.gitconfig setelah pemasangan.

[user]
    name = Budi Santoso
    email = [email protected]

[core]
    editor = nano
    autocrlf = input

[init]
    defaultBranch = main

[alias]
    s  = status --short --branch
    co = checkout
    br = branch --verbose

Empat seksi di dalamnya menjawab empat kebiasaan: [user] mengisi identitas yang dicatat pada setiap commit — tanpa ini commit pertama di mesin baru akan tertolak; [core] memilih editor pesan commit (nano karena ramah pemula — ganti sesuai selera) dan autocrlf = input yang menormalkan CRLF Windows menjadi LF saat berkas masuk repo; [init] defaultBranch = main menghapus pertanyaan nama branch saat git init; dan [alias] membuat tiga singkatan — git s, git co, git br — yang kita pakai di Bagian 3; daftar konfigurasi yang bisa diisi ada di git-config(1) . Tingkatan konfigurasi git (sistem, global, repo) sudah dibedah di Chapter Git dan Version Control ; template ini dipasang sebagai konfigurasi global.

.editorconfig: Konvensi Lintas Editor #

ini
# .editorconfig — konvensi format berkas lintas editor.
# Editor yang memasang plugin EditorConfig (VS Code, Vim, Neovim, dan
# mayoritas editor modern) membaca berkas ini; yang tidak, mengabaikannya.

root = true

[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
indent_style = space
indent_size = 4

[*.md]
trim_trailing_whitespace = false

[Makefile]
indent_style = tab

[*.{yml,yaml,json,toml}]
indent_size = 2

editorconfig menyelesaikan debat klasik tim — tab atau spasi, berapa lebar indentnya — dengan satu berkas yang dibaca hampir semua editor. Baris root = true menandai berkas ini sebagai puncak pencarian, sehingga proyek di bawahnya tidak menyerap aturan dari folder yang lebih atas. Seksi [*] memberlakukan UTF-8, akhir baris LF, baris baru di akhir berkas, dan indent spasi selebar empat; tiga seksi berikutnya meluluskannya: Markdown dipertahankan spasi trailnya (dua spasi di akhir baris adalah ganti baris yang sah), Makefile wajib tab karena resepnya memang begitu, dan berkas yml/json memakai indent dua. Yang menarik dari .editorconfig adalah kegagalannya yang aman: editor tanpa plugin cukup mengabaikannya — berbeda dari tool yang memaksa, konvensi ini menyapa yang mau.

Folder dotfiles bisa didistribusikan ke home dengan dua cara, dan keduanya punya pendukung setia. Tabel berikut membandingkan yang penting bagi kita:

AspekSymlinkCopy
Sumber kebenaransatu: berkas di repo dotfilesdua: template dan berkas terpasang
Sunting di repo, efeknyaaktif begitu shell dibuka ulangbaru aktif setelah setup.sh diulang
Repo dipindah atau dihapussymlink menggantung, konfigurasi patahberkas di home tetap utuh
Perubahan lokal (di luar repo)bercampur ke repo, mudah lupa di-committerdeteksi: cmp beda, backup dibuat
Cocok untukmesin yang repo-nya menetapmesin baru dan mesin cadangan

Kita memilih copy. Alasannya bukan bahwa symlink salah, melainkan bahwa profil risiko kita berbeda: proyek ini justru dirancang untuk mesin baru yang foldernya bisa berpindah, dan alur kerja kita sudah mengharuskan setup.sh dijalankan ulang setiap kali dotfiles berubah — jadi keunggulan “instan”-nya symlink tidak kita perlukan. Sebaligus, copy membuat perilaku setup.sh mudah diuji: bandingkan berkas terpasang dengan template lewat cmp, selesai.

setup.sh Lengkap: Memasang dengan Backup #

Sekarang tahap 2 dari setup.sh: memasang ketiga dotfile ke home, membuat hook di .bashrc, dan melakukan semuanya dengan dua jaminan — berkas lama ditimpa hanya lewat salinan .bak, dan eksekusi kedua tidak mengubah apa pun. Blok berikut menulis versi lengkap skrip (identik dengan scripts/setup.sh pada bagian Berkas Sumber), lalu mengujinya pada keadaan yang paling menarik: home yang sudah berisi .gitconfig lama:

bash
mkdir -p dev-env/scripts dev-env/config
cat > dev-env/config/.bashrc-dev <<'EOF'
# .bashrc-dev — kustomisasi shell untuk development.
# Dipasang setup.sh sebagai ~/.bashrc-dev, dimuat dari ~/.bashrc.
[ -d "$HOME/.local/bin" ] && PATH="$HOME/.local/bin:$PATH"
alias ll='ls -alF'
mkcd() {
    mkdir -p -- "$1" && cd -- "$1"
}
git_branch() {
    local cabang
    cabang=$(git symbolic-ref --short HEAD 2>/dev/null) || return 0
    printf '(%s) ' "$cabang"
}
PS1='\u@\h:\W$(git_branch)\$ '
EOF
cat > dev-env/config/.gitconfig <<'EOF'
[user]
    name = Budi Santoso
    email = [email protected]
[core]
    editor = nano
    autocrlf = input
[init]
    defaultBranch = main
[alias]
    s  = status --short --branch
    co = checkout
    br = branch --verbose
EOF
cat > dev-env/config/.editorconfig <<'EOF'
root = true
[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
indent_style = space
indent_size = 4
[*.md]
trim_trailing_whitespace = false
[Makefile]
indent_style = tab
[*.{yml,yaml,json,toml}]
indent_size = 2
EOF
cat > dev-env/scripts/setup.sh <<'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
BASH

# simulasi mesin lama: sudah ada .gitconfig versi sebelumnya
printf '[user]\n\tname = Budi Lama\n' > "$HOME/.gitconfig"

echo "== eksekusi pertama =="
bash dev-env/scripts/setup.sh; echo "exit=$?"
echo "== isi home setelah setup =="
ls -a "$HOME"
echo "== bukti: isi identik dengan template =="
diff dev-env/config/.gitconfig "$HOME/.gitconfig" && echo "gitconfig identik"
echo "== hook yang tertulis di .bashrc =="
cat "$HOME/.bashrc"
echo "== backup menyimpan versi lama =="
cat "$HOME/.gitconfig.bak"
echo "== eksekusi kedua (idempoten) =="
bash dev-env/scripts/setup.sh > laporan-2.txt; echo "exit=$?"
tail -n 6 laporan-2.txt

Output yang Dihasilkan:

text
== eksekusi pertama ==
== 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
== tahap 2: dotfiles ==
[PASANG] /home/budi/.bashrc-dev
[BAK]    /home/budi/.gitconfig -> /home/budi/.gitconfig.bak
[PASANG] /home/budi/.gitconfig
[PASANG] /home/budi/.editorconfig
[PASANG] hook .bashrc memuat .bashrc-dev
setup.sh: selesai — lingkungan siap
exit=0
== isi home setelah setup ==
.
..
.bashrc
.bashrc-dev
.editorconfig
.gitconfig
.gitconfig.bak
dev-env
== bukti: isi identik dengan template ==
gitconfig identik
== hook yang tertulis di .bashrc ==

# >>> lingkungan-dev: muat kustomisasi shell >>>
[ -f "$HOME/.bashrc-dev" ] && . "$HOME/.bashrc-dev"
# <<< lingkungan-dev <<<
== backup menyimpan versi lama ==
[user]
	name = Budi Lama
== eksekusi kedua (idempoten) ==
exit=0
== tahap 2: dotfiles ==
[OK]     /home/budi/.bashrc-dev sudah terpasang
[OK]     /home/budi/.gitconfig sudah terpasang
[OK]     /home/budi/.editorconfig sudah terpasang
[OK]     /home/budi/.bashrc sudah memuat .bashrc-dev
setup.sh: selesai — lingkungan siap

Bagian baru skrip ini bekerja karena empat keputusan kecil:

  1. if [ -f "$target" ] && cmp -s "$sumber" "$target": gerbang pertama pasang_dotfile. cmp -s membandingkan isi dua berkas secara diam — keluar nol bila identik. Selama berkas terpasang sama dengan template, fungsi langsung melapor [OK] dan tidak menyentuh apa pun; inilah sumber idempotensi, dan baris [OK] ... sudah terpasang pada eksekusi kedua di atas adalah buktinya.
  2. Urutan cek-mode sebelum backup: cabang MODE = cek diletakkan setelah gerbang cmp tapi sebelum penimpaan, sehingga --check tetap melaporkan status berkas yang sudah benar ([OK]), hanya menahan aksi. Pratinjau yang jujur harus tahu keadaan, bukan sekadar diam.
  3. cp "$target" "$target.bak" sebelum cp "$sumber" "$target": penimpaan selalu didahului salinan lama — di blok di atas, .gitconfig milik “Budi Lama” selamat di .gitconfig.bak sebelum ditimpa template. Backup dibuat hanya saat penimpaan sungguhan terjadi, jadi eksekusi ulang tidak menimpa .bak dengan salinan yang identik.
  4. grep -qF '.bashrc-dev' "$bashrc": kunci anti-dobel untuk hook. Kita tidak men-nge-link .bashrc-dev dari .bashrc dengan cara menimpanya, melainkan menambah empat baris di akhir — dan grep -qF memastikan empat baris itu hanya ditulis sekali, berapa kali pun setup dijalankan. Blok { printf ...; } >> "$bashrc" mengelompokkan empat printf menjadi satu penambahan.

Lingkungan kini lengkap secara konfigurasi: tool diperiksa, dotfile terpasang terkelola, dan hook shell aktif. Sisa pekerjaan bukan lagi menyiapkan mesin, melainkan mempercepat hari kerja — Bagian 3 menutup proyek dengan scaffold proyek baru, alias yang sudah menunggu dipakai, dan cara menjaga setup ini tetap mutakhir.

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 ]