Add initial scripts and project conventions
This commit is contained in:
1 parent
af9f32facc
commit
c777cf115b
14 files changed
+982
-9
No files matched your search
+12
-9
@@ -1,14 +1,17 @@
|
|||||||
# Editor temporary and backup files
|
# Local/editor junk
|
||||||
*~
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
*.swp
|
*.swp
|
||||||
*.swo
|
*.swo
|
||||||
*.bak
|
.vscode/*
|
||||||
.#*
|
!.vscode/extensions.json
|
||||||
|
.idea/
|
||||||
|
|
||||||
# Temporary files and generated logs
|
# Script-local temp/output — scripts should write here if they need
|
||||||
*.tmp
|
# scratch space, never into the repo tree itself
|
||||||
|
tmp/
|
||||||
*.log
|
*.log
|
||||||
|
|
||||||
# Operating system files
|
# Local env overrides, if any script ends up needing one
|
||||||
.DS_Store
|
.env
|
||||||
Thumbs.db
|
.env.local
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
# shellcheck config — applies to every script in this repo via `shellcheck`
|
||||||
|
# run from the repo root. See AGENTS.md for the mandatory-shellcheck rule.
|
||||||
|
|
||||||
|
# Follow sourced files (needed so shellcheck resolves lib/common.sh
|
||||||
|
# correctly instead of flagging unknown functions as undefined).
|
||||||
|
external-sources=true
|
||||||
|
|
||||||
|
shell=bash
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
# AGENTS.md — StrixScripts
|
||||||
|
|
||||||
|
Инструкции за AI coding агенти (Claude Code, GitHub Copilot, Cursor и др.),
|
||||||
|
работещи в този repo. Ако инструментът ти чете `CLAUDE.md` вместо този
|
||||||
|
файл — `CLAUDE.md` е кратък pointer към този документ; съдържанието не се
|
||||||
|
дублира умишлено, за да не се разсинхронизира.
|
||||||
|
|
||||||
|
## Какво е проектът
|
||||||
|
|
||||||
|
Колекция от самостоятелни bash скриптове, организирани по функционални
|
||||||
|
папки. Всеки скрипт решава една конкретна задача и има собствена `.md`
|
||||||
|
документация до себе си.
|
||||||
|
|
||||||
|
## Задължителна структура при всеки нов скрипт
|
||||||
|
|
||||||
|
Когато създаваш нов скрипт, следните стъпки са ЗАДЪЛЖИТЕЛНИ, не опционални
|
||||||
|
— пропускането на която и да е от тях е непълна задача, не "готово с TODO":
|
||||||
|
|
||||||
|
1. **Скриптът и неговият `.md` файл имат еднакво basename**, в една и
|
||||||
|
съща папка: `deploy/restart-nginx.sh` → `deploy/restart-nginx.md`.
|
||||||
|
Виж `templates/script-template.md` за структурата, която `.md` файлът
|
||||||
|
трябва да следва.
|
||||||
|
|
||||||
|
2. **Скриптът source-ва `lib/common.sh`** и извиква
|
||||||
|
`common::show_help_if_requested "$@"` веднага след дефиниране на
|
||||||
|
`SCRIPT_HELP` heredoc-а, преди какъвто и да е друг argument parsing.
|
||||||
|
Виж `templates/script-template.sh` за точния pattern и
|
||||||
|
`examples/hello-world.sh` за работещ пример.
|
||||||
|
|
||||||
|
3. **`--help` (и `-h`) работи и показва пълна информация** — какво прави
|
||||||
|
скриптът, какви опции приема, поне един пример за извикване. Това е
|
||||||
|
съдържанието на `SCRIPT_HELP`, не отделна имплементация — не пиши
|
||||||
|
ръчен `if [[ "$1" == "--help" ]]` в скрипта, common.sh вече го прави.
|
||||||
|
|
||||||
|
4. **В `README.md` (корена на проекта) се добавя ред в секцията на
|
||||||
|
съответната категория папка** — кратко описание (едно изречение) +
|
||||||
|
relative link към `.md` файла на скрипта. Пълното описание живее само
|
||||||
|
в скрипт-специфичния `.md`, README не го дублира.
|
||||||
|
|
||||||
|
5. **Скриптът минава `shellcheck` без warnings** (виж `.shellcheckrc` в
|
||||||
|
корена). Ако warning-ът е false positive, добави `# shellcheck disable=SCxxxx`
|
||||||
|
с коментар защо точно на реда, не глобално в файла.
|
||||||
|
|
||||||
|
6. **`set -euo pipefail`** в началото на всеки скрипт, веднага след shebang
|
||||||
|
реда и коментарния блок. Без изключения — ако скрипт умишлено трябва да
|
||||||
|
продължи след грешка в конкретна команда, обгради само тази команда с
|
||||||
|
`|| true` или явна проверка, не маха global `set -e`.
|
||||||
|
|
||||||
|
## Конвенции за именуване и стил
|
||||||
|
|
||||||
|
- **Файлови имена:** kebab-case, описателни глагол+обект (`backup-nextcloud.sh`,
|
||||||
|
не `backup.sh` или `script1.sh`).
|
||||||
|
- **Категория папки:** функционална група (напр. `backup/`, `network/`,
|
||||||
|
`deploy/`) — създавай нова папка само когато скриптовете вътре наистина
|
||||||
|
споделят функционален домейн, не по проект или по дата.
|
||||||
|
- **Всички коментари в кода — само на английски**, независимо от езика на
|
||||||
|
разговора с агента. `.md` документацията и `README.md` могат да остават
|
||||||
|
на български — те са за човека, не за компилатора/shellcheck.
|
||||||
|
- **Грешки:** винаги през `common::die "message"`, никога суров
|
||||||
|
`echo ... >&2; exit 1` — за консистентен формат на грешките между
|
||||||
|
скриптове.
|
||||||
|
- **Статус/progress съобщения:** през `common::log`, който пише на stderr,
|
||||||
|
за да остане stdout чист за реален output, който може да бъде pipe-нат
|
||||||
|
или captured от друг скрипт/потребител.
|
||||||
|
|
||||||
|
## `lib/common.sh` — НЕ дублирай логика от него
|
||||||
|
|
||||||
|
Ако скрипт се нуждае от поведение, което `common.sh` не предоставя (напр.
|
||||||
|
retry logic, temp file cleanup trap, конкретен validation helper) —
|
||||||
|
разшири `common.sh`, не пиши локална версия вътре в отделния скрипт. Целта
|
||||||
|
е всеки скрипт да остава кратък и всички споделят едно и също поведение
|
||||||
|
за грешки/help/logging.
|
||||||
|
|
||||||
|
## `examples/` и `templates/` — само референция
|
||||||
|
|
||||||
|
`examples/hello-world.sh` е работещ демонстрационен скрипт, показващ
|
||||||
|
пълната конвенция накратко. `templates/` съдържа празни шаблони за копиране.
|
||||||
|
Нито едната папка не съдържа реални production скриптове — не ги викай от
|
||||||
|
други скриптове или automation, и не добавяй реални utility скриптове там.
|
||||||
|
|
||||||
|
## Статус
|
||||||
|
|
||||||
|
Проектът е в начален етап — само structural скелет (`lib/common.sh`,
|
||||||
|
`templates/`, `examples/hello-world.sh`) съществува. Реални функционални
|
||||||
|
категории папки и скриптове предстои да бъдат създадени.
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
# CLAUDE.md
|
||||||
|
|
||||||
|
Пълните инструкции за работа в този repo са в [`AGENTS.md`](./AGENTS.md).
|
||||||
|
Прочети него първо — този файл е само pointer, за да не поддържаме два
|
||||||
|
дублиращи се документа, които се разсинхронизират.
|
||||||
|
|
||||||
|
Claude-специфично: няма допълнителни инструкции извън `AGENTS.md` в момента.
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
# erase-storage.sh
|
||||||
|
|
||||||
|
Изтрива един дял с еднократен запис на нули или псевдослучайни данни, или
|
||||||
|
използва поддържания от устройството ATA Secure Erase/NVMe Sanitize за цял диск.
|
||||||
|
|
||||||
|
## Какво прави
|
||||||
|
|
||||||
|
При подаване на дял, скриптът записва точно размера на целия дял един път. По подразбиране
|
||||||
|
използва нули; може да се избере и `/dev/urandom`. Изисква root права,
|
||||||
|
проверява че устройството не е монтирано, не е активен swap и не участва в
|
||||||
|
root filesystem, след което иска потвърждение чрез въвеждане на точния път.
|
||||||
|
|
||||||
|
При подаване на цял диск скриптът проверява поддръжката преди да продължи.
|
||||||
|
За NVMe избира наличния Sanitize метод в следния ред: Crypto Erase, Block
|
||||||
|
Erase, Overwrite. За ATA избира Enhanced Security Erase, ако устройството го
|
||||||
|
обявява, иначе стандартен Security Erase. Ако методът не може да бъде
|
||||||
|
потвърден, устройството е замразено/вече е с активирана ATA защита или
|
||||||
|
информацията за capability е неясна, скриптът отказва да продължи. Преди
|
||||||
|
изтриването на цял диск показва отделно предупреждение и изисква две
|
||||||
|
потвърждения, като второто е точното име на устройството.
|
||||||
|
|
||||||
|
**Изтриването е необратимо и при режим за цял диск засяга всички дялове.**
|
||||||
|
Не прекъсвай захранването или връзката с диска. При прекъсване на ATA
|
||||||
|
Security Erase устройството може да остане заключено с временната парола,
|
||||||
|
посочена в съобщението за грешка. Secure Erase/Sanitize зависи от коректната
|
||||||
|
реализация във firmware на устройството; скриптът не може да гарантира
|
||||||
|
възстановимост или физическо състояние на носителя.
|
||||||
|
|
||||||
|
## Изисквания
|
||||||
|
|
||||||
|
- Linux, Bash и root права (`sudo`)
|
||||||
|
- `lsblk`, `findmnt`, `swapon`, `dd` и GNU/Linux стандартни помощни команди
|
||||||
|
- За NVMe: `nvme-cli` и `jq`
|
||||||
|
- За ATA: `hdparm`
|
||||||
|
|
||||||
|
## Употреба
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo ./HDD/erase-storage.sh --help
|
||||||
|
sudo ./HDD/erase-storage.sh /dev/sda1
|
||||||
|
sudo ./HDD/erase-storage.sh /dev/nvme0n1p2 random
|
||||||
|
sudo ./HDD/erase-storage.sh /dev/sda
|
||||||
|
sudo ./HDD/erase-storage.sh /dev/nvme0n1
|
||||||
|
```
|
||||||
|
|
||||||
|
`dev/sda1` без начална наклонена черта също се приема като `/dev/sda1`.
|
||||||
|
|
||||||
|
| Аргумент | Описание |
|
||||||
|
|----------|----------|
|
||||||
|
| `DEVICE` | Block device — дял или цял диск |
|
||||||
|
| `zero` | Един проход с нули върху дял; режим по подразбиране |
|
||||||
|
| `random` | Един проход с псевдослучайни данни върху дял |
|
||||||
|
| `-h`, `--help` | Показва помощта и приключва |
|
||||||
|
|
||||||
|
Режимът `zero`/`random` не е Secure Erase и не гарантира изтриване на
|
||||||
|
пренасочени или недостъпни физически сектори. При SSD не използвай обикновен
|
||||||
|
запис към дял като гаранция за заличаване; използвай само успешно завършен
|
||||||
|
Sanitize/Secure Erase, поддържан от устройството.
|
||||||
Executable
+240
@@ -0,0 +1,240 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
#
|
||||||
|
# erase-storage.sh — overwrite a partition or securely erase a whole disk.
|
||||||
|
# Full documentation: see erase-storage.md in this same folder.
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
# shellcheck source=lib/common.sh
|
||||||
|
source "${SCRIPT_DIR}/../lib/common.sh"
|
||||||
|
|
||||||
|
SCRIPT_HELP=$(cat <<'HELP'
|
||||||
|
Usage: sudo ./HDD/erase-storage.sh DEVICE [zero|random]
|
||||||
|
|
||||||
|
For a partition, writes one full pass of zeros (default) or pseudorandom data.
|
||||||
|
For a whole disk, detects and runs a supported ATA Secure Erase or NVMe Sanitize
|
||||||
|
operation. Whole-disk erasure requires two separate confirmations.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
DEVICE Partition (for example /dev/sda1) or whole disk
|
||||||
|
zero Write zeros to a partition (default)
|
||||||
|
random Write pseudorandom data to a partition
|
||||||
|
|
||||||
|
Options:
|
||||||
|
-h, --help Show this help message and exit
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
sudo ./HDD/erase-storage.sh /dev/sda1
|
||||||
|
sudo ./HDD/erase-storage.sh /dev/nvme0n1p2 random
|
||||||
|
sudo ./HDD/erase-storage.sh /dev/sda
|
||||||
|
sudo ./HDD/erase-storage.sh /dev/nvme0n1
|
||||||
|
HELP
|
||||||
|
)
|
||||||
|
|
||||||
|
common::show_help_if_requested "$@"
|
||||||
|
|
||||||
|
[[ $# -ge 1 && $# -le 2 ]] || common::die "expected DEVICE [zero|random]; run with --help for usage"
|
||||||
|
|
||||||
|
DEVICE_INPUT="$1"
|
||||||
|
if [[ "$DEVICE_INPUT" == dev/* ]]; then
|
||||||
|
DEVICE_INPUT="/$DEVICE_INPUT"
|
||||||
|
fi
|
||||||
|
[[ "$DEVICE_INPUT" == /dev/* ]] || common::die "DEVICE must be a path under /dev"
|
||||||
|
|
||||||
|
FILL_MODE="${2:-zero}"
|
||||||
|
case "$FILL_MODE" in
|
||||||
|
zero|random)
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
common::die "partition fill mode must be 'zero' or 'random'"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
for required_command in findmnt lsblk readlink swapon date dd sleep; do
|
||||||
|
common::require_cmd "$required_command"
|
||||||
|
done
|
||||||
|
|
||||||
|
[[ "$EUID" -eq 0 ]] || common::die "this script must be run as root (for example, with sudo)"
|
||||||
|
[[ -b "$DEVICE_INPUT" ]] || common::die "$DEVICE_INPUT does not exist or is not a block device"
|
||||||
|
|
||||||
|
DEVICE="$(readlink -f -- "$DEVICE_INPUT")" || common::die "cannot resolve device path: $DEVICE_INPUT"
|
||||||
|
DEVICE_TYPE="$(lsblk -dnro TYPE -- "$DEVICE")" || common::die "cannot determine device type"
|
||||||
|
[[ "$DEVICE_TYPE" == "part" || "$DEVICE_TYPE" == "disk" ]] \
|
||||||
|
|| common::die "only a partition or a whole disk is accepted (detected: $DEVICE_TYPE)"
|
||||||
|
|
||||||
|
ROOT_SOURCE="$(findmnt -n -o SOURCE /)" || common::die "cannot determine the root filesystem device"
|
||||||
|
ROOT_SOURCE="${ROOT_SOURCE%%\[*}"
|
||||||
|
[[ -b "$ROOT_SOURCE" ]] || common::die "cannot safely resolve the root filesystem device ($ROOT_SOURCE)"
|
||||||
|
ROOT_DEVICE="$(readlink -f -- "$ROOT_SOURCE")" || common::die "cannot resolve the root filesystem device"
|
||||||
|
ROOT_TREE="$(lsblk --inverse --list --noheadings --paths --output PATH -- "$ROOT_DEVICE")" \
|
||||||
|
|| common::die "cannot determine the devices backing the root filesystem"
|
||||||
|
|
||||||
|
while IFS= read -r root_device; do
|
||||||
|
[[ -n "$root_device" ]] || continue
|
||||||
|
if [[ "$(readlink -f -- "$root_device")" == "$DEVICE" ]]; then
|
||||||
|
common::die "$DEVICE backs the root filesystem; refusing to erase it"
|
||||||
|
fi
|
||||||
|
done <<< "$ROOT_TREE"
|
||||||
|
|
||||||
|
DEVICE_TREE="$(lsblk --list --noheadings --paths --output PATH -- "$DEVICE")" \
|
||||||
|
|| common::die "cannot inspect devices below $DEVICE"
|
||||||
|
ACTIVE_SWAP_DEVICES="$(swapon --show --noheadings --raw --output NAME)" \
|
||||||
|
|| common::die "cannot inspect active swap devices"
|
||||||
|
|
||||||
|
while IFS= read -r child_device; do
|
||||||
|
[[ -n "$child_device" ]] || continue
|
||||||
|
if findmnt -rn -S "$child_device" >/dev/null 2>&1; then
|
||||||
|
common::die "$child_device is mounted; unmount it before running this script"
|
||||||
|
fi
|
||||||
|
|
||||||
|
while IFS= read -r swap_device; do
|
||||||
|
[[ -n "$swap_device" ]] || continue
|
||||||
|
if [[ -b "$swap_device" ]]; then
|
||||||
|
swap_source="$swap_device"
|
||||||
|
else
|
||||||
|
swap_mount_source="$(findmnt -n -o SOURCE -T "$swap_device")" \
|
||||||
|
|| common::die "cannot determine the filesystem backing active swap file $swap_device"
|
||||||
|
swap_mount_source="${swap_mount_source%%\[*}"
|
||||||
|
[[ -b "$swap_mount_source" ]] \
|
||||||
|
|| common::die "cannot safely resolve the block device backing active swap file $swap_device"
|
||||||
|
swap_source="$swap_mount_source"
|
||||||
|
fi
|
||||||
|
|
||||||
|
swap_source_tree="$(lsblk --inverse --list --noheadings --paths --output PATH -- "$swap_source")" \
|
||||||
|
|| common::die "cannot determine the devices backing active swap $swap_device"
|
||||||
|
while IFS= read -r swap_backing_device; do
|
||||||
|
[[ -n "$swap_backing_device" ]] || continue
|
||||||
|
if [[ "$(readlink -f -- "$swap_backing_device")" == "$(readlink -f -- "$child_device")" ]]; then
|
||||||
|
common::die "$child_device backs active swap ($swap_device); disable it before running this script"
|
||||||
|
fi
|
||||||
|
done <<< "$swap_source_tree"
|
||||||
|
done <<< "$ACTIVE_SWAP_DEVICES"
|
||||||
|
done <<< "$DEVICE_TREE"
|
||||||
|
|
||||||
|
if [[ "$DEVICE_TYPE" == "part" ]]; then
|
||||||
|
[[ $# -eq 1 || "$FILL_MODE" == "zero" || "$FILL_MODE" == "random" ]] \
|
||||||
|
|| common::die "invalid fill mode; use 'zero' or 'random'"
|
||||||
|
DEVICE_SIZE="$(lsblk -dnro SIZE --bytes -- "$DEVICE")" || common::die "cannot determine partition size"
|
||||||
|
|
||||||
|
common::log "WARNING: this will irreversibly overwrite all data on partition $DEVICE ($DEVICE_SIZE bytes)."
|
||||||
|
read -r -p "Type the exact device path '$DEVICE' to continue: " CONFIRM
|
||||||
|
[[ "$CONFIRM" == "$DEVICE" ]] || common::die "operation cancelled; no data was changed"
|
||||||
|
|
||||||
|
common::log "Writing one full $FILL_MODE pass to $DEVICE."
|
||||||
|
if [[ "$FILL_MODE" == "zero" ]]; then
|
||||||
|
dd if=/dev/zero of="$DEVICE" bs=4M count="$DEVICE_SIZE" iflag=count_bytes conv=fsync status=progress
|
||||||
|
else
|
||||||
|
dd if=/dev/urandom of="$DEVICE" bs=4M count="$DEVICE_SIZE" iflag=count_bytes conv=fsync status=progress
|
||||||
|
fi
|
||||||
|
common::log "Partition overwrite completed successfully."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
[[ $# -eq 1 ]] || common::die "a fill mode is only valid for a partition"
|
||||||
|
DEVICE_SIZE="$(lsblk -dnro SIZE --bytes -- "$DEVICE")" || common::die "cannot determine disk size"
|
||||||
|
DEVICE_NAME="${DEVICE##*/}"
|
||||||
|
ERASE_METHOD=""
|
||||||
|
ERASE_TOOL=""
|
||||||
|
NVME_ACTION=""
|
||||||
|
NVME_ACTION_NAME=""
|
||||||
|
ATA_ERASE_OPTION=""
|
||||||
|
|
||||||
|
if [[ "$DEVICE_NAME" =~ ^(nvme[0-9]+)n[0-9]+$ ]]; then
|
||||||
|
common::require_cmd nvme
|
||||||
|
common::require_cmd jq
|
||||||
|
NVME_CONTROLLER="/dev/${BASH_REMATCH[1]}"
|
||||||
|
NVME_IDENTIFY="$(nvme id-ctrl "$NVME_CONTROLLER" -o json)" \
|
||||||
|
|| common::die "could not read NVMe controller capabilities"
|
||||||
|
SANICAP="$(jq -er '.sanicap' <<< "$NVME_IDENTIFY")" \
|
||||||
|
|| common::die "NVMe controller did not report SANICAP"
|
||||||
|
[[ "$SANICAP" =~ ^[0-9]+$ ]] || common::die "invalid NVMe SANICAP value: $SANICAP"
|
||||||
|
SANICAP_VALUE=$((SANICAP))
|
||||||
|
|
||||||
|
if (( SANICAP_VALUE & 1 )); then
|
||||||
|
NVME_ACTION=4
|
||||||
|
NVME_ACTION_NAME="Crypto Erase"
|
||||||
|
elif (( SANICAP_VALUE & 2 )); then
|
||||||
|
NVME_ACTION=2
|
||||||
|
NVME_ACTION_NAME="Block Erase"
|
||||||
|
elif (( SANICAP_VALUE & 4 )); then
|
||||||
|
NVME_ACTION=3
|
||||||
|
NVME_ACTION_NAME="Overwrite"
|
||||||
|
else
|
||||||
|
common::die "NVMe controller does not advertise a supported Sanitize action"
|
||||||
|
fi
|
||||||
|
ERASE_METHOD="NVMe Sanitize: $NVME_ACTION_NAME"
|
||||||
|
ERASE_TOOL="nvme"
|
||||||
|
else
|
||||||
|
common::require_cmd hdparm
|
||||||
|
common::require_cmd awk
|
||||||
|
common::require_cmd grep
|
||||||
|
ATA_IDENTIFY="$(hdparm -I "$DEVICE" 2>&1)" || common::die "could not read ATA security capabilities"
|
||||||
|
ATA_SECURITY="$(awk '
|
||||||
|
/^[[:space:]]*Security:/ { in_security = 1; print; next }
|
||||||
|
in_security && /^[^[:space:]]/ { exit }
|
||||||
|
in_security { print }
|
||||||
|
' <<< "$ATA_IDENTIFY")"
|
||||||
|
[[ -n "$ATA_SECURITY" ]] || common::die "device did not report an ATA Security section"
|
||||||
|
grep -Eiq '^[[:space:]]+supported[[:space:]]*$' <<< "$ATA_SECURITY" \
|
||||||
|
|| common::die "device does not report ATA Security Erase support"
|
||||||
|
grep -Eiq '^[[:space:]]+not frozen[[:space:]]*$' <<< "$ATA_SECURITY" \
|
||||||
|
|| common::die "ATA security is frozen or its state could not be determined; refusing to continue"
|
||||||
|
grep -Eiq '^[[:space:]]+not enabled[[:space:]]*$' <<< "$ATA_SECURITY" \
|
||||||
|
|| common::die "ATA security is already enabled; refusing to risk leaving the disk locked"
|
||||||
|
|
||||||
|
if grep -Eiq 'supported:[[:space:]]+enhanced erase' <<< "$ATA_SECURITY"; then
|
||||||
|
ATA_ERASE_OPTION="--security-erase-enhanced"
|
||||||
|
ERASE_METHOD="ATA Enhanced Security Erase"
|
||||||
|
else
|
||||||
|
ATA_ERASE_OPTION="--security-erase"
|
||||||
|
ERASE_METHOD="ATA Security Erase"
|
||||||
|
fi
|
||||||
|
ERASE_TOOL="hdparm"
|
||||||
|
fi
|
||||||
|
|
||||||
|
common::log "WARNING: whole-disk operation on $DEVICE ($DEVICE_SIZE bytes)."
|
||||||
|
common::log "Detected supported method: $ERASE_METHOD. ALL DATA on this disk will be destroyed."
|
||||||
|
read -r -p "First confirmation: type ERASE to continue: " FIRST_CONFIRM
|
||||||
|
[[ "$FIRST_CONFIRM" == "ERASE" ]] || common::die "operation cancelled; no data was changed"
|
||||||
|
common::log "Final warning: $ERASE_METHOD will erase the entire disk $DEVICE, including every partition."
|
||||||
|
read -r -p "Final confirmation: type the exact device name '$DEVICE_NAME': " FINAL_CONFIRM
|
||||||
|
[[ "$FINAL_CONFIRM" == "$DEVICE_NAME" ]] || common::die "operation cancelled; no data was changed"
|
||||||
|
|
||||||
|
if [[ "$ERASE_TOOL" == "nvme" ]]; then
|
||||||
|
common::log "Starting $ERASE_METHOD on $DEVICE. The device may continue sanitizing after the command returns."
|
||||||
|
nvme sanitize "$DEVICE" --sanact="$NVME_ACTION"
|
||||||
|
|
||||||
|
while true; do
|
||||||
|
SANITIZE_LOG="$(nvme sanitize-log "$DEVICE" -o json)" \
|
||||||
|
|| common::die "could not read NVMe Sanitize status; the operation may still be running"
|
||||||
|
SANITIZE_STATUS="$(jq -er '.sstat' <<< "$SANITIZE_LOG")" \
|
||||||
|
|| common::die "NVMe Sanitize status was missing or invalid"
|
||||||
|
[[ "$SANITIZE_STATUS" =~ ^[0-9]+$ ]] || common::die "invalid NVMe Sanitize status: $SANITIZE_STATUS"
|
||||||
|
|
||||||
|
case $((SANITIZE_STATUS & 7)) in
|
||||||
|
1|4)
|
||||||
|
common::log "NVMe Sanitize completed successfully."
|
||||||
|
break
|
||||||
|
;;
|
||||||
|
2)
|
||||||
|
common::log "NVMe Sanitize is still in progress; checking again in 30 seconds."
|
||||||
|
sleep 30
|
||||||
|
;;
|
||||||
|
3)
|
||||||
|
common::die "NVMe Sanitize reported failure; consult the device sanitize log"
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
common::die "NVMe Sanitize is not running or reported an unknown status ($SANITIZE_STATUS)"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
else
|
||||||
|
common::log "Starting $ERASE_METHOD on $DEVICE. Do not interrupt power or disconnect the drive."
|
||||||
|
ATA_PASSWORD="strix-erase"
|
||||||
|
hdparm --user-master u --security-set-pass "$ATA_PASSWORD" "$DEVICE"
|
||||||
|
if ! hdparm --user-master u "$ATA_ERASE_OPTION" "$ATA_PASSWORD" "$DEVICE"; then
|
||||||
|
common::die "ATA erase failed; the disk may remain security-locked with the temporary password '$ATA_PASSWORD'"
|
||||||
|
fi
|
||||||
|
common::log "$ERASE_METHOD completed successfully."
|
||||||
|
fi
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
# repair-hdd.sh
|
||||||
|
|
||||||
|
Презаписва избран диск, за да задейства remap на проблемни сектори, след
|
||||||
|
което изпълнява SMART extended self-test и показва финален отчет.
|
||||||
|
|
||||||
|
## Какво прави
|
||||||
|
|
||||||
|
Скриптът записва целия block device с нули или псевдослучайни данни. След
|
||||||
|
това стартира продължителен SMART self-test и изчаква очакваното му време,
|
||||||
|
преди да покаже резултата и SMART атрибутите. Изходът се записва в
|
||||||
|
`/var/log/hdd-repair/`.
|
||||||
|
|
||||||
|
**Операцията унищожава всички данни на избрания диск.** Преди запис скриптът
|
||||||
|
проверява дали дискът участва в root filesystem, проверява за монтирани
|
||||||
|
дъщерни устройства и активен swap, и изисква изрично потвърждение `YES`.
|
||||||
|
Ако дъщерно устройство не може да се демонтира, скриптът прекратява
|
||||||
|
операцията.
|
||||||
|
|
||||||
|
Използвай само с правилно идентифициран диск, който можеш безопасно да
|
||||||
|
изтриеш. Изисква пряка SATA връзка; `smartctl` не използва `-d sat`, затова
|
||||||
|
USB адаптер може да изисква ръчна промяна на извикванията в скрипта.
|
||||||
|
|
||||||
|
## Изисквания
|
||||||
|
|
||||||
|
- Linux и Bash
|
||||||
|
- Root права (`sudo`)
|
||||||
|
- `smartctl` от пакета smartmontools
|
||||||
|
- `findmnt`, `lsblk`, `readlink`, `swapon`, `umount` и стандартните GNU/Linux
|
||||||
|
помощни команди, използвани от скрипта
|
||||||
|
- OpenSSL е незадължителен; при липса режимът `random` използва `/dev/urandom`
|
||||||
|
|
||||||
|
## Употреба
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo ./HDD/repair-hdd.sh --help
|
||||||
|
sudo ./HDD/repair-hdd.sh /dev/sdX
|
||||||
|
sudo ./HDD/repair-hdd.sh /dev/sdX zero
|
||||||
|
sudo ./HDD/repair-hdd.sh /dev/sdX null
|
||||||
|
sudo ./HDD/repair-hdd.sh /dev/sdX random
|
||||||
|
```
|
||||||
|
|
||||||
|
| Аргумент | Описание |
|
||||||
|
|----------|----------|
|
||||||
|
| `DEVICE` | Целият диск, например `/dev/sdX`, не дял от него |
|
||||||
|
| `zero` | Записва нули; режим по подразбиране |
|
||||||
|
| `null` | Синоним на `zero` |
|
||||||
|
| `random` | Записва псевдослучайни данни |
|
||||||
|
| `-h`, `--help` | Показва помощта и приключва |
|
||||||
|
|
||||||
|
Изпълнението може да продължи часове. Не прекъсвай захранването или връзката
|
||||||
|
с диска по време на записа или SMART теста. SMART remap не възстановява
|
||||||
|
данни и не гарантира, че дискът ще стане надежден; запази резервно копие и
|
||||||
|
подмени диск с повтарящи се грешки.
|
||||||
Executable
+217
@@ -0,0 +1,217 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
#
|
||||||
|
# repair-hdd.sh — overwrite a disk to trigger remapping of pending/bad sectors,
|
||||||
|
# then run a SMART extended self-test and print a final report.
|
||||||
|
# Full documentation: see repair-hdd.md in this same folder.
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
# shellcheck source=lib/common.sh
|
||||||
|
source "${SCRIPT_DIR}/../lib/common.sh"
|
||||||
|
|
||||||
|
SCRIPT_HELP=$(cat <<'HELP'
|
||||||
|
Usage: sudo ./HDD/repair-hdd.sh DEVICE [zero|null|random]
|
||||||
|
|
||||||
|
Overwrites the selected disk, then runs a SMART extended self-test. This
|
||||||
|
destroys all data on DEVICE. Use only with a directly connected SATA disk;
|
||||||
|
USB adapters may require adding -d sat to the smartctl calls.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
DEVICE Whole-disk block device, for example /dev/sdX
|
||||||
|
zero Write zeros (default; also accepted as "null")
|
||||||
|
random Write pseudorandom data
|
||||||
|
|
||||||
|
Options:
|
||||||
|
-h, --help Show this help message and exit
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
sudo ./HDD/repair-hdd.sh /dev/sdX
|
||||||
|
sudo ./HDD/repair-hdd.sh /dev/sdX random
|
||||||
|
HELP
|
||||||
|
)
|
||||||
|
|
||||||
|
common::show_help_if_requested "$@"
|
||||||
|
|
||||||
|
[[ $# -le 2 ]] || common::die "too many arguments; run with --help for usage"
|
||||||
|
LOG_DIR="/var/log/hdd-repair"
|
||||||
|
DEVICE="${1:-}"
|
||||||
|
FILL_MODE="${2:-zero}"
|
||||||
|
|
||||||
|
# --- Валидация на входа ---------------------------------------------------
|
||||||
|
|
||||||
|
if [[ -z "$DEVICE" ]]; then
|
||||||
|
common::die "a whole-disk block device is required; run with --help for usage"
|
||||||
|
fi
|
||||||
|
|
||||||
|
case "$FILL_MODE" in
|
||||||
|
zero|null)
|
||||||
|
FILL_MODE="zero"
|
||||||
|
;;
|
||||||
|
random)
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
common::die "fill mode must be 'zero', 'null', or 'random'"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
for required_command in findmnt lsblk readlink swapon mkdir date tee smartctl grep umount dd sync sleep head base64; do
|
||||||
|
common::require_cmd "$required_command"
|
||||||
|
done
|
||||||
|
|
||||||
|
if [[ "$EUID" -ne 0 ]]; then
|
||||||
|
common::die "this script must be run as root (for example, with sudo)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ ! -b "$DEVICE" ]]; then
|
||||||
|
common::die "$DEVICE does not exist or is not a block device"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Refuse the root filesystem device and any block device it depends on.
|
||||||
|
TARGET_DEVICE="$(readlink -f -- "$DEVICE")" || common::die "cannot resolve device path: $DEVICE"
|
||||||
|
ROOT_SOURCE="$(findmnt -n -o SOURCE /)" || common::die "cannot determine the root filesystem device"
|
||||||
|
ROOT_SOURCE="${ROOT_SOURCE%%\[*}"
|
||||||
|
ROOT_DEVICE="$(readlink -f -- "$ROOT_SOURCE")" || common::die "cannot resolve root device path: $ROOT_SOURCE"
|
||||||
|
ROOT_DEVICE_TREE="$(lsblk --inverse --list --noheadings --paths --output PATH "$ROOT_DEVICE")" \
|
||||||
|
|| common::die "cannot determine the devices backing the root filesystem"
|
||||||
|
|
||||||
|
while IFS= read -r root_device; do
|
||||||
|
[[ -n "$root_device" ]] || continue
|
||||||
|
if [[ "$(readlink -f -- "$root_device")" == "$TARGET_DEVICE" ]]; then
|
||||||
|
common::die "$DEVICE backs the root filesystem; refusing to overwrite it"
|
||||||
|
fi
|
||||||
|
done <<< "$ROOT_DEVICE_TREE"
|
||||||
|
|
||||||
|
DEVICE_TREE="$(lsblk --list --noheadings --paths --output PATH "$TARGET_DEVICE")" \
|
||||||
|
|| common::die "cannot inspect block devices below $DEVICE"
|
||||||
|
|
||||||
|
while IFS= read -r child_device; do
|
||||||
|
[[ -n "$child_device" ]] || continue
|
||||||
|
while IFS= read -r swap_device; do
|
||||||
|
[[ -n "$swap_device" ]] || continue
|
||||||
|
if [[ "$(readlink -f -- "$swap_device")" == "$(readlink -f -- "$child_device")" ]]; then
|
||||||
|
common::die "$child_device is active swap; disable it before running this script"
|
||||||
|
fi
|
||||||
|
done < <(swapon --show --noheadings --raw --output NAME)
|
||||||
|
done <<< "$DEVICE_TREE"
|
||||||
|
|
||||||
|
if [[ "$FILL_MODE" == "random" ]] && ! command -v openssl >/dev/null 2>&1; then
|
||||||
|
common::log "OpenSSL is unavailable; random mode will use /dev/urandom."
|
||||||
|
fi
|
||||||
|
|
||||||
|
mkdir -p "$LOG_DIR"
|
||||||
|
TIMESTAMP="$(date +%Y%m%d-%H%M%S)"
|
||||||
|
MAIN_LOG="$LOG_DIR/hdd-repair-${DEVICE##*/}-${TIMESTAMP}.log"
|
||||||
|
|
||||||
|
# Send all output to both the console and the log file.
|
||||||
|
exec > >(tee -a "$MAIN_LOG") 2>&1
|
||||||
|
|
||||||
|
common::log "=============================================================="
|
||||||
|
common::log "HDD Repair — $DEVICE"
|
||||||
|
common::log "Write mode: $FILL_MODE"
|
||||||
|
common::log "Started: $(date '+%Y-%m-%d %H:%M:%S')"
|
||||||
|
common::log "Log: $MAIN_LOG"
|
||||||
|
common::log "=============================================================="
|
||||||
|
|
||||||
|
# --- Disk information before the operation --------------------------------
|
||||||
|
|
||||||
|
common::log "--- Device identification ---"
|
||||||
|
smartctl -i "$DEVICE" || true
|
||||||
|
|
||||||
|
common::log "--- SMART values before overwrite ---"
|
||||||
|
smartctl -A "$DEVICE" | grep -E \
|
||||||
|
"Reallocated_Sector_Ct|Current_Pending_Sector|Offline_Uncorrectable|UDMA_CRC_Error_Count|Power_On_Hours" \
|
||||||
|
|| common::log "SMART read failed; check the connection."
|
||||||
|
|
||||||
|
common::log "WARNING: ALL DATA ON $DEVICE WILL BE DESTROYED."
|
||||||
|
read -r -p "Type uppercase YES to confirm: " CONFIRM
|
||||||
|
if [[ "$CONFIRM" != "YES" ]]; then
|
||||||
|
common::die "operation cancelled; no changes were made"
|
||||||
|
fi
|
||||||
|
|
||||||
|
while IFS= read -r child_device; do
|
||||||
|
[[ -n "$child_device" ]] || continue
|
||||||
|
if findmnt -n -S "$child_device" >/dev/null 2>&1; then
|
||||||
|
common::log "Unmounting $child_device..."
|
||||||
|
umount "$child_device" || common::die "could not unmount $child_device; refusing to overwrite $DEVICE"
|
||||||
|
fi
|
||||||
|
done <<< "$DEVICE_TREE"
|
||||||
|
|
||||||
|
# --- Phase 1: overwrite the disk to trigger sector remapping ---------------
|
||||||
|
|
||||||
|
common::log "--- Phase 1: overwriting $DEVICE ---"
|
||||||
|
common::log "Started: $(date '+%Y-%m-%d %H:%M:%S')"
|
||||||
|
common::log "This may take hours, depending on disk size and speed."
|
||||||
|
|
||||||
|
DD_START=$(date +%s)
|
||||||
|
DD_RESULT=0
|
||||||
|
|
||||||
|
if [[ "$FILL_MODE" == "zero" ]]; then
|
||||||
|
RANDOM_SOURCE_DESC="/dev/zero"
|
||||||
|
dd if=/dev/zero of="$DEVICE" bs=4M conv=fsync,noerror status=progress || DD_RESULT=$?
|
||||||
|
else
|
||||||
|
# Use OpenSSL for faster pseudorandom generation when available.
|
||||||
|
if command -v openssl >/dev/null 2>&1; then
|
||||||
|
RANDOM_SOURCE_DESC="openssl enc (AES-256-CTR PRNG)"
|
||||||
|
openssl enc -aes-256-ctr -pass pass:"$(head -c 32 /dev/urandom | base64)" \
|
||||||
|
-nosalt < /dev/zero 2>/dev/null | \
|
||||||
|
dd of="$DEVICE" bs=4M conv=fsync,noerror status=progress || DD_RESULT=$?
|
||||||
|
else
|
||||||
|
RANDOM_SOURCE_DESC="/dev/urandom (по-бавно, openssl не е намерен)"
|
||||||
|
dd if=/dev/urandom of="$DEVICE" bs=4M conv=fsync,noerror status=progress || DD_RESULT=$?
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
common::log "Data source: $RANDOM_SOURCE_DESC"
|
||||||
|
if [[ "$DD_RESULT" -eq 0 ]]; then
|
||||||
|
common::log "dd completed successfully (no write I/O errors)."
|
||||||
|
else
|
||||||
|
common::log "dd exited with status $DD_RESULT; the disk may have reached its end."
|
||||||
|
fi
|
||||||
|
DD_END=$(date +%s)
|
||||||
|
common::log "Overwrite finished: $(date '+%Y-%m-%d %H:%M:%S')"
|
||||||
|
common::log "Duration: $(( (DD_END - DD_START) / 3600 ))h $(( ((DD_END - DD_START) % 3600) / 60 ))m"
|
||||||
|
|
||||||
|
sync
|
||||||
|
common::log "Waiting 10 seconds before SMART verification."
|
||||||
|
sleep 10
|
||||||
|
|
||||||
|
common::log "--- SMART values after overwrite ---"
|
||||||
|
smartctl -A "$DEVICE" | grep -E \
|
||||||
|
"Reallocated_Sector_Ct|Current_Pending_Sector|Offline_Uncorrectable|UDMA_CRC_Error_Count" \
|
||||||
|
|| common::log "SMART read failed."
|
||||||
|
|
||||||
|
# --- Phase 2: SMART extended self-test -------------------------------------
|
||||||
|
|
||||||
|
common::log "--- Phase 2: starting SMART extended self-test ---"
|
||||||
|
smartctl -t long "$DEVICE"
|
||||||
|
|
||||||
|
# Read the estimated duration from smartctl, falling back to 360 minutes.
|
||||||
|
EST_MIN="$(smartctl -c "$DEVICE" 2>/dev/null | grep -oP 'Extended self-test routine\s*\K[0-9]+' || true)"
|
||||||
|
EST_MIN="${EST_MIN:-360}"
|
||||||
|
WAIT_SECONDS=$(( (EST_MIN + 10) * 60 ))
|
||||||
|
|
||||||
|
common::log "Expected self-test duration: about ${EST_MIN} minutes."
|
||||||
|
common::log "Waiting $(( WAIT_SECONDS / 60 )) minutes before reading the result."
|
||||||
|
common::log "The drive firmware runs the self-test while this script waits."
|
||||||
|
|
||||||
|
sleep "$WAIT_SECONDS"
|
||||||
|
|
||||||
|
common::log "--- Self-test result ---"
|
||||||
|
smartctl -l selftest "$DEVICE"
|
||||||
|
|
||||||
|
# --- Final report ----------------------------------------------------------
|
||||||
|
|
||||||
|
common::log "=============================================================="
|
||||||
|
common::log "Final SMART report — $DEVICE"
|
||||||
|
common::log "Finished: $(date '+%Y-%m-%d %H:%M:%S')"
|
||||||
|
common::log "=============================================================="
|
||||||
|
smartctl -A "$DEVICE" | grep -E \
|
||||||
|
"Reallocated_Sector_Ct|Current_Pending_Sector|Offline_Uncorrectable|UDMA_CRC_Error_Count|Reallocated_Event_Count"
|
||||||
|
smartctl -H "$DEVICE"
|
||||||
|
common::log "Full log: $MAIN_LOG"
|
||||||
|
common::log "Compare these values with the initial values in the log:"
|
||||||
|
common::log "Current_Pending_Sector should decrease (ideally to 0)."
|
||||||
|
common::log "Reallocated_Sector_Ct may increase as sectors are remapped."
|
||||||
|
common::log "If pending sectors remain, the drive may be unable to write them."
|
||||||
|
common::log "=============================================================="
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
# StrixScripts
|
||||||
|
|
||||||
|
Колекция от самостоятелни bash скриптове, организирани по функционални
|
||||||
|
папки. Всеки скрипт има собствена `.md` документация до себе си с пълно
|
||||||
|
описание, изисквания и примери.
|
||||||
|
|
||||||
|
За AI агенти, работещи в repo-то: виж [`AGENTS.md`](./AGENTS.md) за
|
||||||
|
задължителните конвенции при създаване на нов скрипт — структура,
|
||||||
|
`--help` поведение, shellcheck, документация.
|
||||||
|
|
||||||
|
## Структура
|
||||||
|
|
||||||
|
```
|
||||||
|
strixscripts/
|
||||||
|
├── lib/
|
||||||
|
│ └── common.sh споделени helper функции (--help, error handling, logging)
|
||||||
|
├── templates/ шаблони за нов скрипт + неговия .md файл
|
||||||
|
├── examples/
|
||||||
|
│ └── hello-world.sh работещ пример, демонстриращ пълната конвенция
|
||||||
|
├── HDD/
|
||||||
|
│ ├── repair-hdd.sh презапис на диск и SMART extended self-test
|
||||||
|
│ └── erase-storage.sh презапис на дял или Secure Erase/Sanitize на диск
|
||||||
|
└── <категория>/ функционална група скриптове (предстои)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Как се използва скрипт
|
||||||
|
|
||||||
|
Всеки скрипт поддържа `--help`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./<категория>/<script-name>.sh --help
|
||||||
|
```
|
||||||
|
|
||||||
|
## Категории
|
||||||
|
|
||||||
|
<!-- Всяка нова категория папка получава своя секция тук, с кратко
|
||||||
|
описание на всеки скрипт в нея + link към пълната документация.
|
||||||
|
Пълното описание живее в скрипт-специфичния .md, не тук. -->
|
||||||
|
|
||||||
|
### examples/ (референция, не production скриптове)
|
||||||
|
|
||||||
|
- [`hello-world.sh`](./examples/hello-world.md) — демонстрационен скрипт,
|
||||||
|
показващ конвенцията за нов скрипт в repo-то.
|
||||||
|
|
||||||
|
### HDD/
|
||||||
|
|
||||||
|
- [`repair-hdd.sh`](./HDD/repair-hdd.md) — презаписва избран диск и
|
||||||
|
изпълнява SMART extended self-test.
|
||||||
|
- [`erase-storage.sh`](./HDD/erase-storage.md) — презаписва дял с един проход
|
||||||
|
или използва поддържан Secure Erase/Sanitize за цял диск.
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
# hello-world.sh
|
||||||
|
|
||||||
|
Reference example script — prints a greeting. Demonstrates the required
|
||||||
|
structure every script in this repo must follow: `common.sh` sourcing,
|
||||||
|
`SCRIPT_HELP` heredoc, `--help` handling, and argument parsing with
|
||||||
|
`common::die` on invalid input.
|
||||||
|
|
||||||
|
**This is not a real utility.** Copy its structure when writing an actual
|
||||||
|
script; don't invoke it from other scripts or automation.
|
||||||
|
|
||||||
|
## What it does
|
||||||
|
|
||||||
|
Prints `Hello, <name>!` to stdout, where `<name>` defaults to `world` or
|
||||||
|
whatever is passed via `--name`.
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
- Bash 4+ (uses `set -euo pipefail`, no external commands beyond common.sh)
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./examples/hello-world.sh [OPTIONS]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Options
|
||||||
|
|
||||||
|
| Option | Description |
|
||||||
|
|--------|-------------|
|
||||||
|
| `-h`, `--help` | Show help message and exit |
|
||||||
|
| `-n`, `--name NAME` | Name to greet (default: `world`) |
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Default greeting
|
||||||
|
./examples/hello-world.sh
|
||||||
|
# → Hello, world!
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Custom name
|
||||||
|
./examples/hello-world.sh --name Danail
|
||||||
|
# → Hello, Danail!
|
||||||
|
```
|
||||||
Executable
+50
@@ -0,0 +1,50 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
#
|
||||||
|
# hello-world.sh — reference example demonstrating the StrixScripts
|
||||||
|
# conventions (see AGENTS.md). Not a real utility script — copy this
|
||||||
|
# pattern when creating an actual script, don't call this one from
|
||||||
|
# anywhere real.
|
||||||
|
#
|
||||||
|
# Full documentation: see hello-world.md in this same folder.
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
# shellcheck source=lib/common.sh
|
||||||
|
source "${SCRIPT_DIR}/../lib/common.sh"
|
||||||
|
|
||||||
|
SCRIPT_HELP=$(cat <<'HELP'
|
||||||
|
Usage: hello-world.sh [-n NAME]
|
||||||
|
|
||||||
|
Prints a greeting. Reference example only — demonstrates the SCRIPT_HELP
|
||||||
|
pattern, argument parsing, and common.sh usage that every real script in
|
||||||
|
this repo should follow.
|
||||||
|
|
||||||
|
Options:
|
||||||
|
-h, --help Show this help message and exit
|
||||||
|
-n, --name NAME Name to greet (default: world)
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
./hello-world.sh
|
||||||
|
./hello-world.sh --name Danail
|
||||||
|
HELP
|
||||||
|
)
|
||||||
|
|
||||||
|
common::show_help_if_requested "$@"
|
||||||
|
|
||||||
|
name="world"
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
-n|--name)
|
||||||
|
[[ $# -ge 2 ]] || common::die "--name requires a value"
|
||||||
|
name="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
common::die "unknown argument: $1"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
common::log "Greeting ${name}..."
|
||||||
|
echo "Hello, ${name}!"
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
#
|
||||||
|
# common.sh — shared helper functions for all StrixScripts scripts.
|
||||||
|
#
|
||||||
|
# Every script in this repo MUST source this file. It provides:
|
||||||
|
# - --help handling driven by a script-defined SCRIPT_HELP heredoc
|
||||||
|
# - consistent error/log output
|
||||||
|
# - a command-existence guard
|
||||||
|
#
|
||||||
|
# Usage from a script:
|
||||||
|
# SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
# source "${SCRIPT_DIR}/../lib/common.sh" # adjust ../ depth to match nesting
|
||||||
|
#
|
||||||
|
# Do NOT duplicate this logic inside individual scripts — if a script needs
|
||||||
|
# behavior this file doesn't provide, extend this file instead, so every
|
||||||
|
# script stays consistent.
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# common::show_help_if_requested scans argv for -h/--help and, if present,
|
||||||
|
# prints the calling script's $SCRIPT_HELP and exits 0.
|
||||||
|
#
|
||||||
|
# Call this IMMEDIATELY after defining SCRIPT_HELP and BEFORE any other
|
||||||
|
# argument parsing — --help must win regardless of what else is on the
|
||||||
|
# command line (including invalid flags).
|
||||||
|
common::show_help_if_requested() {
|
||||||
|
local arg
|
||||||
|
for arg in "$@"; do
|
||||||
|
if [[ "$arg" == "-h" || "$arg" == "--help" ]]; then
|
||||||
|
if [[ -z "${SCRIPT_HELP:-}" ]]; then
|
||||||
|
echo "Error: SCRIPT_HELP is not defined for this script." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "$SCRIPT_HELP"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
# common::die prints an error to stderr and exits with the given code
|
||||||
|
# (default 1). Use this for every fatal error instead of a raw `exit 1`,
|
||||||
|
# so error output is consistent across all scripts.
|
||||||
|
common::die() {
|
||||||
|
local message="$1"
|
||||||
|
local code="${2:-1}"
|
||||||
|
echo "Error: ${message}" >&2
|
||||||
|
exit "$code"
|
||||||
|
}
|
||||||
|
|
||||||
|
# common::log prints a timestamped status line to stderr. stdout is left
|
||||||
|
# clean for actual script output that might be piped or captured by a
|
||||||
|
# caller — never write status/progress messages to stdout.
|
||||||
|
common::log() {
|
||||||
|
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" >&2
|
||||||
|
}
|
||||||
|
|
||||||
|
# common::require_cmd fails fast with a clear message if a required
|
||||||
|
# external command is missing from PATH, instead of letting the script
|
||||||
|
# fail later with a cryptic "command not found".
|
||||||
|
common::require_cmd() {
|
||||||
|
local cmd="$1"
|
||||||
|
if ! command -v "$cmd" &>/dev/null; then
|
||||||
|
common::die "required command '${cmd}' not found on PATH"
|
||||||
|
fi
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
# <script-name>.sh
|
||||||
|
|
||||||
|
<One or two sentence summary — same tone as the README entry, but here you
|
||||||
|
can go into more detail immediately.>
|
||||||
|
|
||||||
|
## What it does
|
||||||
|
|
||||||
|
<Explain what the script does step by step, in prose. If it touches
|
||||||
|
external systems (network, filesystem paths outside the repo, remote
|
||||||
|
hosts, cron), say so explicitly here — a reader deciding whether to run
|
||||||
|
this needs to know the blast radius before they run it.>
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
- <External commands the script depends on, e.g. `curl`, `jq`, `ssh`>
|
||||||
|
- <Any required environment variables, config files, or permissions>
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./<path-to-script>/<script-name>.sh [OPTIONS]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Options
|
||||||
|
|
||||||
|
| Option | Description |
|
||||||
|
|--------|-------------|
|
||||||
|
| `-h`, `--help` | Show help message and exit |
|
||||||
|
| `<flag>` | <description> |
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# <what this example does>
|
||||||
|
./<script-name>.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# <what this example does>
|
||||||
|
./<script-name>.sh --example value
|
||||||
|
```
|
||||||
|
|
||||||
|
## Notes / Caveats
|
||||||
|
|
||||||
|
<Anything non-obvious: destructive operations, required order relative to
|
||||||
|
other scripts, assumptions about the environment it runs in, known
|
||||||
|
limitations. If there's nothing notable, delete this section — don't
|
||||||
|
leave a placeholder "None" in real docs.>
|
||||||
Executable
+44
@@ -0,0 +1,44 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
#
|
||||||
|
# <script-name>.sh — <one-line description of what this script does>
|
||||||
|
#
|
||||||
|
# Full documentation: see <script-name>.md in this same folder.
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
# shellcheck source=lib/common.sh
|
||||||
|
# Adjust the relative depth below to match this script's folder nesting
|
||||||
|
# relative to lib/ — e.g. "../../lib/common.sh" if the script is two
|
||||||
|
# folders deep from the repo root.
|
||||||
|
source "${SCRIPT_DIR}/../lib/common.sh"
|
||||||
|
|
||||||
|
SCRIPT_HELP=$(cat <<'HELP'
|
||||||
|
Usage: <script-name>.sh [OPTIONS]
|
||||||
|
|
||||||
|
<One paragraph explaining what the script does and when to use it.>
|
||||||
|
|
||||||
|
Options:
|
||||||
|
-h, --help Show this help message and exit
|
||||||
|
<-x, --example> <describe other options here>
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
./<script-name>.sh
|
||||||
|
./<script-name>.sh --example value
|
||||||
|
HELP
|
||||||
|
)
|
||||||
|
|
||||||
|
# --help must be checked before any other argument parsing.
|
||||||
|
common::show_help_if_requested "$@"
|
||||||
|
|
||||||
|
# --- Argument parsing (if the script takes options) ---
|
||||||
|
# TODO: parse "$@" here if needed. Use common::die for invalid arguments,
|
||||||
|
# not a raw echo+exit, so error formatting stays consistent.
|
||||||
|
|
||||||
|
# --- Script logic ---
|
||||||
|
|
||||||
|
common::log "Starting <script-name>..."
|
||||||
|
|
||||||
|
# TODO: actual script logic goes here.
|
||||||
|
|
||||||
|
common::log "Done."
|
||||||
Reference in new issue
Block a user