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 #
# .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:
- Baris
[ -d "$HOME/.local/bin" ] && PATH=...: folder~/.local/binadalah lokasi standar alat milik user — hasilpip install --user, skrip sendiri, dan sejenisnya. Prefiks[ -d ... ] &&memastikan baris hanya menambahPATHbila foldernya ada, sehingga berkas ini aman dimuat di mesin mana pun tanpa menaruh folder hantu diPATH. - 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. - Fungsi
mkcd:mkdir -pmembuat folder bertingkat sekali jalan,cdlangsung 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 . - Fungsi
git_branch: mengembalikan nama branch aktif atau diam di luar repo (|| return 0menelan kegagalansymbolic-ref). Kata kuncilocalmenjaga variabelnya tidak bocor ke shell pemanggil. - 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 #
# .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 --verboseEmpat 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 #
# .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 = 2editorconfig 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.
Symlink atau Copy? #
Folder dotfiles bisa didistribusikan ke home dengan dua cara, dan keduanya punya pendukung setia. Tabel berikut membandingkan yang penting bagi kita:
| Aspek | Symlink | Copy |
|---|---|---|
| Sumber kebenaran | satu: berkas di repo dotfiles | dua: template dan berkas terpasang |
| Sunting di repo, efeknya | aktif begitu shell dibuka ulang | baru aktif setelah setup.sh diulang |
| Repo dipindah atau dihapus | symlink menggantung, konfigurasi patah | berkas di home tetap utuh |
| Perubahan lokal (di luar repo) | bercampur ke repo, mudah lupa di-commit | terdeteksi: cmp beda, backup dibuat |
| Cocok untuk | mesin yang repo-nya menetap | mesin 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:
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.txtOutput yang Dihasilkan:
== 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 siapBagian baru skrip ini bekerja karena empat keputusan kecil:
if [ -f "$target" ] && cmp -s "$sumber" "$target": gerbang pertamapasang_dotfile.cmp -smembandingkan 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 terpasangpada eksekusi kedua di atas adalah buktinya.- Urutan cek-mode sebelum backup: cabang
MODE = cekdiletakkan setelah gerbangcmptapi sebelum penimpaan, sehingga--checktetap melaporkan status berkas yang sudah benar ([OK]), hanya menahan aksi. Pratinjau yang jujur harus tahu keadaan, bukan sekadar diam. cp "$target" "$target.bak"sebelumcp "$sumber" "$target": penimpaan selalu didahului salinan lama — di blok di atas,.gitconfigmilik “Budi Lama” selamat di.gitconfig.baksebelum ditimpa template. Backup dibuat hanya saat penimpaan sungguhan terjadi, jadi eksekusi ulang tidak menimpa.bakdengan salinan yang identik.grep -qF '.bashrc-dev' "$bashrc": kunci anti-dobel untuk hook. Kita tidak men-nge-link.bashrc-devdari.bashrcdengan cara menimpanya, melainkan menambah empat baris di akhir — dangrep -qFmemastikan empat baris itu hanya ditulis sekali, berapa kali pun setup dijalankan. Blok{ printf ...; } >> "$bashrc"mengelompokkan empatprintfmenjadi 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#!/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#!/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#!/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 ]