Lingkungan yang siap mengubah biaya memulai sesuatu. Setiap ide baru kini berhadapan dengan ritual yang sama: mkdir, git init, salin .gitignore dari proyek lama, tulis Makefile dari ingatan, commit pertama — lima belas menit pekerjaan yang tidak menambah satu baris kode pun. Pada bagian penutup ini kita hapus ritual itu dengan proyek-baru.sh: satu perintah yang men-scaffold repo git lengkap dengan commit awal, lalu kita rangkai alias dan fungsi shell yang membuat hari kerja terasa pendek, dan menutup dengan cara menjaga seluruh setup tetap mutakhir tanpa mengingatnya.
proyek-baru.sh: Scaffold dalam Satu Perintah #
Skrip ini menerima nama proyek dan jenis (c, python, atau node), lalu membuat folder berisi repo git branch main dengan satu commit awal: README.md, .gitignore dari template sesuai jenis, Makefile, dan src/ dengan berkas awal. Dua keputusan desainnya tegas: skrip tidak pernah menimpa — nama yang sudah dipakai ditolak dengan kode keluar 2 — dan commit awal dibuat otomatis sehingga git log tidak pernah kosong. Identitas commit diambil dari konfigurasi git Anda; bila belum diisi, skrip memakai nilai bawaan yang bisa di-override lewat environment — pola yang sama dengan STAMP pada proyek-proyek sebelumnya.
Blok berikut menulis proyek-baru.sh (identik dengan scripts/proyek-baru.sh pada bagian Berkas Sumber), membuat proyek contoh aplikasi-tes jenis c, lalu membuktikan hasilnya: struktur lengkap, status bersih, riwayat commit, dan proyek yang benar-benar bisa dibangun. Variabel GIT_AUTHOR_DATE dan kawan-kawannya kita bekukan agar commit — dan hash-nya — dapat direproduksi:
mkdir -p dev-env/scripts dev-env/config
cat > dev-env/config/gitignore-c <<'EOF'
# gitignore-c — hasil build dan berkas sementara C/C++.
*.o
*.a
*.so
core
EOF
cat > dev-env/config/gitignore-python <<'EOF'
# gitignore-python — bytecode, virtual environment, dan distribusi.
__pycache__/
*.pyc
.venv/
venv/
dist/
*.egg-info/
EOF
cat > dev-env/scripts/proyek-baru.sh <<'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
BASH
export GIT_AUTHOR_NAME="Budi Santoso" GIT_AUTHOR_EMAIL="[email protected]"
export GIT_COMMITTER_NAME="$GIT_AUTHOR_NAME" GIT_COMMITTER_EMAIL="$GIT_AUTHOR_EMAIL"
export GIT_AUTHOR_DATE="2026-01-15 09:00:00 +0000"
export GIT_COMMITTER_DATE="2026-01-15 09:00:00 +0000"
bash dev-env/scripts/proyek-baru.sh aplikasi-tes c
echo "== struktur yang dibuat =="
find aplikasi-tes -not -path '*/.git' -not -path '*/.git/*' | sort
echo "== status repo =="
git -C aplikasi-tes status --short --branch
echo "== riwayat =="
git -C aplikasi-tes log --oneline --decorate
echo "== build dan jalankan =="
make -s -C aplikasi-tes run
echo "== binary tidak mengotori repo =="
git -C aplikasi-tes status --porcelain | wc -lOutput yang Dihasilkan:
proyek siap: aplikasi-tes (jenis c)
cd aplikasi-tes && make run
== struktur yang dibuat ==
aplikasi-tes
aplikasi-tes/.gitignore
aplikasi-tes/Makefile
aplikasi-tes/README.md
aplikasi-tes/src
aplikasi-tes/src/main.c
== status repo ==
## main
== riwayat ==
36b90bf (HEAD -> main) commit awal: kerangka c
== build dan jalankan ==
== binary tidak mengotori repo ==
0Bagian-bagian kuncinya:
- Validasi berlapis dengan
case: jumlah argumen, jenis, lalu nama — masing-masing ditolak dengan pesan ke stderr dan kode keluar berbeda. Pola''|-*)menolak nama kosong dan nama berawalan-(yang bakal tertelan sebagai opsi oleh perintah lain), sedangkan*/*)menolak nama berisi/yang akan membuat folder tak terduga. [ -e "$NAMA" ] && gagal ...: gerbang tidak-menimpa. Skrip yang membuat proyek atas nama kita tidak berhak menghapus pekerjaan lama — nama yang sudah ada ditolak total, bukan “dilanjutkan”.git init -q -b main: repo dibuat dengan branch utama bernamamainsejak awal, konsisten denganinit.defaultBranchdi.gitconfigBagian 2 — opsi-qmeredam sapaangitagar laporan skrip tetap bersih — daftar lengkapnya di git-init(1) .printf '# binary hasil build make\n/%s\n' "$NAMA" >> .gitignore: satu-satunya baris.gitignoreyang spesifik proyek. Templategitignore-cmenangani pola generik (*.o,core), sedangkan binary hasilmakebernama sama dengan proyek — pola/aplikasi-tesyang dijangkar ke akar repo membuatnya diabaikan tanpa ikut mengabaikan berkas lain yang kebetulan bernama sama di folder dalam.- Makefile untuk
cditulis lewat heredoc tanpa kutip (<<MAKE):$NAMAmemang harus diekspansi menjadi nama proyek, sedangkan setiap$(...)perlu tetap literal untuk make — makanya ditulis\$(BIN). Resep make wajib diawali karakter tab, bukan spasi;Makefilepada bagian Berkas Sumber menyimpannya apa adanya. - Blok identitas
git config --get user.name: pada mesin yang.gitconfig-nya sudah terpasang (hasil Bagian 2), commit memakai identitas Anda; pada mesin telanjang,: "${GIT_AUTHOR_NAME:=Pengembang}"mengisi nilai bawaan yang bisa di-override dari environment — pola parameter expansion yang sama dengan yang kita pakai berulang kali sepanjang course. - Empat variabel
GIT_*DATE/GIT_*_NAMEdi blok uji: author, committer, dan waktunya dibekukan penuh sebelum skrip dijalankan. Commit git adalah hash dari isi tree, identitas, dan waktu — bekukan semuanya dan hash-nya pun sama di setiap eksekusi, seperti barise850a0fpada output di atas.
Perhatikan urutan bukti di akhir blok: find memperlihatkan struktur termasuk .gitignore (sementara isi .git disembunyikan), git status --short --branch bersih di branch main, git log menampilkan satu commit awal, make run menghasilkan halo, dunia, dan hitungan git status --porcelain tetap 0 setelah build — binary memang diabaikan, bukan sekadar tidak kelihatan.
Alias Git dan Fungsi Shell Sehari-hari #
Scaffold menyingkat awal proyek; alias menyingkat setiap hari setelahnya. .gitconfig dari Bagian 2 memuat tiga alias yang paling sering dipakai:
| Alias | Perintah asli | Untuk |
|---|---|---|
git s | git status --short --branch | kondisi repo sekali lirik |
git co | git checkout | pindah branch |
git br | git branch --verbose | daftar branch beserta commit terakhirnya |
Blok berikut menyiapkan repo contoh kecil — satu berkas, satu commit, persis kondisi yang ditinggalkan proyek-baru.sh — dengan GIT_CONFIG_GLOBAL mengarah ke .gitconfig template agar sesi ini benar-benar memakai konfigurasi Bagian 2, lalu menguji ketiga alias di atasnya dan mkcd dari .bashrc-dev:
mkdir -p dev-env/config
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/.bashrc-dev <<'EOF'
# .bashrc-dev — kustomisasi shell untuk development.
[ -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
# repo contoh: satu berkas, satu commit — kondisi yang ditinggalkan
# proyek-baru.sh pada blok sebelumnya
export GIT_AUTHOR_NAME="Budi Santoso" GIT_AUTHOR_EMAIL="[email protected]"
export GIT_COMMITTER_NAME="$GIT_AUTHOR_NAME" GIT_COMMITTER_EMAIL="$GIT_AUTHOR_EMAIL"
export GIT_AUTHOR_DATE="2026-01-15 09:00:00 +0000"
export GIT_COMMITTER_DATE="2026-01-15 09:00:00 +0000"
mkdir -p aplikasi-tes
(
cd aplikasi-tes
git init -q -b main
printf '# aplikasi-tes\n\nProyek aplikasi-tes (jenis: c) dibuat oleh proyek-baru.sh.\n' > README.md
git add -A
git commit -q -m "commit awal: kerangka c"
)
echo "== git s: status ringkas lewat alias =="
GIT_CONFIG_GLOBAL="$PWD/dev-env/config/.gitconfig" git -C aplikasi-tes s
echo "== git co: pindah ke cabang baru lewat alias =="
GIT_CONFIG_GLOBAL="$PWD/dev-env/config/.gitconfig" git -C aplikasi-tes co -b fitur-cli
echo "== git br: daftar cabang lewat alias =="
GIT_CONFIG_GLOBAL="$PWD/dev-env/config/.gitconfig" git -C aplikasi-tes br
echo "== mkcd: buat folder dan langsung masuk =="
bash -c '. dev-env/config/.bashrc-dev; mkcd latihan-mkcd; pwd'Output yang Dihasilkan:
== git s: status ringkas lewat alias ==
## main
== git co: pindah ke cabang baru lewat alias ==
== git br: daftar cabang lewat alias ==
* fitur-cli 4118335 commit awal: kerangka c
main 4118335 commit awal: kerangka c
== mkcd: buat folder dan langsung masuk ==
/home/budi/latihan-mkcdDua catatan dari blok itu. Pertama, GIT_CONFIG_GLOBAL adalah cara git modern mengarahkan konfigurasi global ke berkas lain tanpa menyentuh home — di komputer Anda yang sudah menjalankan setup.sh, tiga alias itu tinggal dipakai begitu saja. Kedua, mkcd diuji lewat bash -c '. dev-env/config/.bashrc-dev; ...' — memuat berkas kustomisasi lalu langsung memakai fungsinya; pwd yang tercetak di akhir membuktikan shell memang berpindah ke folder yang baru dibuatnya.
Menjaga Setup Tetap Mutakhir #
Terakhir, lingkungan yang bisa dibangun ulang baru berguna bila juga bisa diperbarui. Alurnya satu arah dan pendek: sunting dotfile di repo, commit, lalu terapkan ulang:
cd ~/dotfiles
vi config/.bashrc-dev # atau editor apa pun
git commit -am "tambah alias ga"
bash scripts/setup.sh # terapkan ke home — idempotenKarena setup.sh aman diulang, langkah terakhir boleh dijalankan kapan saja tanpa takut dobel. Membuatnya berjalan sendiri tinggal menambah satu baris crontab — pola yang kita bahas tuntas di proyek Otomasi dengan cron dan systemd
:
# mingguan, jumat 21:00: tarik dotfiles terbaru lalu terapkan ulang
0 21 * * 5 cd /home/budi/dotfiles && git pull --quiet && bash scripts/setup.sh >> log/setup.log 2>&1Kode keluar 3 dari setup.sh — tool inti hilang atau terlalu tua — menjadi alarm alami: log mingguan yang berisi [HILANG] atau [TUA] adalah tanda mesin mulai tertinggal, cukup dilihat sekali seminggu.
Dengan ini proyek — dan Course Linux ini — ditutup penuh. Kita mulai dari terminal dan perintah pertama, menapaki filesystem, permission, process, dan jaringan, lalu mengakhiri dengan membangun mesin kerja yang tidak lagi bergantung pada ingatan: tool diperiksa skrip, konfigurasi hidup di repo, proyek baru lahir satu perintah, dan test suite tests/run-tests.sh berdiri siap membuktikan semuanya kapan pun diminta. Lingkungan development yang bisa dibangun ulang adalah hadiah terbaik untuk diri kita sendiri: mulai proyek berikutnya, bukan mengulang pengaturannya.
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 ]