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:

bash
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 -l

Output yang Dihasilkan:

text
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 ==
0

Bagian-bagian kuncinya:

  1. 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.
  2. [ -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”.
  3. git init -q -b main: repo dibuat dengan branch utama bernama main sejak awal, konsisten dengan init.defaultBranch di .gitconfig Bagian 2 — opsi -q meredam sapaan git agar laporan skrip tetap bersih — daftar lengkapnya di git-init(1) .
  4. printf '# binary hasil build make\n/%s\n' "$NAMA" >> .gitignore: satu-satunya baris .gitignore yang spesifik proyek. Template gitignore-c menangani pola generik (*.o, core), sedangkan binary hasil make bernama sama dengan proyek — pola /aplikasi-tes yang dijangkar ke akar repo membuatnya diabaikan tanpa ikut mengabaikan berkas lain yang kebetulan bernama sama di folder dalam.
  5. Makefile untuk c ditulis lewat heredoc tanpa kutip (<<MAKE): $NAMA memang harus diekspansi menjadi nama proyek, sedangkan setiap $(...) perlu tetap literal untuk make — makanya ditulis \$(BIN). Resep make wajib diawali karakter tab, bukan spasi; Makefile pada bagian Berkas Sumber menyimpannya apa adanya.
  6. 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.
  7. Empat variabel GIT_*DATE/GIT_*_NAME di 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 baris e850a0f pada 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:

AliasPerintah asliUntuk
git sgit status --short --branchkondisi repo sekali lirik
git cogit checkoutpindah branch
git brgit branch --verbosedaftar 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:

bash
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:

text
== 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-mkcd

Dua 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:

bash
cd ~/dotfiles
vi config/.bashrc-dev          # atau editor apa pun
git commit -am "tambah alias ga"
bash scripts/setup.sh           # terapkan ke home — idempoten

Karena 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 :

bash
# 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>&1

Kode 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
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 ]