READMD.md
· 706 B · Markdown
Surowy
# Ubuntu Sans Nerd Font Setup
A lightweight script to automatically fetch and install the latest **Ubuntu Sans Nerd Font** for Ubuntu Desktop.
## Quick Install
Run the following command in your terminal:
```bash
bash -c "$(curl -fsSL https://opengist.rmrf.online/weehong/2de15ba0106a475fa41215159203a63b/raw/HEAD/install_font.sh)"
```
## Overview
* **Always Latest:** Dynamically pulls the newest release from the official Nerd Fonts GitHub.
* **No Sudo Needed:** Installs safely to your local user directory (`~/.local/share/fonts/UbuntuSans`).
* **Ready to Use:** Automatically updates the font cache (`fc-cache`) so fonts are available immediately.
* **Dependencies:** Requires `curl` and `unzip`.
Ubuntu Sans Nerd Font Setup
A lightweight script to automatically fetch and install the latest Ubuntu Sans Nerd Font for Ubuntu Desktop.
Quick Install
Run the following command in your terminal:
bash -c "$(curl -fsSL https://opengist.rmrf.online/weehong/2de15ba0106a475fa41215159203a63b/raw/HEAD/install_font.sh)"
Overview
- Always Latest: Dynamically pulls the newest release from the official Nerd Fonts GitHub.
- No Sudo Needed: Installs safely to your local user directory (
~/.local/share/fonts/UbuntuSans). - Ready to Use: Automatically updates the font cache (
fc-cache) so fonts are available immediately. - Dependencies: Requires
curlandunzip.
install-1password.sh
· 3.3 KiB · Bash
Surowy
#!/usr/bin/env bash
# install-1password.sh — Install 1Password desktop from the official 1Password APT repo.
# Hardened: arch-aware, signed-by keyring, debsig verification dir, idempotent, --dry-run.
set -euo pipefail
IFS=$'\n\t'
readonly SCRIPT_NAME="${0##*/}"
DRY_RUN=0
usage() {
cat <<EOF
Usage: sudo $SCRIPT_NAME [--dry-run] [--help]
Configures the official 1Password APT repository (with debsig policy and
keyring) and installs the 1Password desktop application. Idempotent: safe to
re-run.
Options:
--dry-run Print actions without executing.
--help, -h Show this help.
EOF
}
log() { printf '\033[1;34m[%s]\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*"; }
warn() { printf '\033[1;33m[%s] WARN:\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*" >&2; }
die() { printf '\033[1;31m[%s] ERROR:\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*" >&2; exit 1; }
run() { if (( DRY_RUN )); then printf ' DRY-RUN: %s\n' "$*"; else eval "$@"; fi; }
trap 'rc=$?; (( rc )) && printf "\033[1;31m[%s] failed at line %s (exit %d)\033[0m\n" "${SCRIPT_NAME%.sh}" "$LINENO" "$rc" >&2' ERR
while (( $# )); do
case "$1" in
--dry-run) DRY_RUN=1 ;;
-h|--help) usage; exit 0 ;;
*) die "Unknown argument: $1 (try --help)" ;;
esac
shift
done
(( EUID == 0 )) || die "Must run as root. Try: sudo $SCRIPT_NAME"
[[ -r /etc/os-release ]] || die "/etc/os-release not found."
# shellcheck disable=SC1091
. /etc/os-release
case "${ID:-}:${ID_LIKE:-}" in
*ubuntu*|*debian*) : ;;
*) die "Unsupported distro: ${PRETTY_NAME:-unknown}." ;;
esac
# 1Password publishes builds for amd64 and arm64.
ARCH="$(dpkg --print-architecture)"
case "$ARCH" in
amd64|arm64) : ;;
*) die "1Password is only available for amd64/arm64 (detected: $ARCH)." ;;
esac
log "Detected: ${PRETTY_NAME:-unknown}, arch: $ARCH"
export DEBIAN_FRONTEND=noninteractive
KEYRING=/etc/apt/keyrings/1password-archive-keyring.gpg
SOURCES=/etc/apt/sources.list.d/1password.list
DEBSIG_POLICY_DIR=/etc/debsig/policies/AC2D62742012EA22
DEBSIG_KEYRING_DIR=/usr/share/debsig/keyrings/AC2D62742012EA22
log "Installing prerequisites..."
run "apt-get update -qq"
run "apt-get install -y curl gpg ca-certificates"
log "Configuring 1Password APT repository..."
run "install -d -m 0755 /etc/apt/keyrings"
if [[ ! -s "$KEYRING" ]]; then
run "curl -fsSL https://downloads.1password.com/linux/keys/1password.asc | gpg --dearmor -o '$KEYRING'"
run "chmod 0644 '$KEYRING'"
fi
DESIRED_SRC="deb [arch=${ARCH} signed-by=${KEYRING}] https://downloads.1password.com/linux/debian/${ARCH} stable main"
if [[ ! -f "$SOURCES" ]] || ! grep -qxF "$DESIRED_SRC" "$SOURCES"; then
run "printf '%s\n' '$DESIRED_SRC' > '$SOURCES'"
fi
log "Installing debsig policy (verifies package signatures on install)..."
run "install -d -m 0755 '$DEBSIG_POLICY_DIR' '$DEBSIG_KEYRING_DIR'"
if [[ ! -s "${DEBSIG_POLICY_DIR}/1password.pol" ]]; then
run "curl -fsSL https://downloads.1password.com/linux/debian/debsig/1password.pol -o '${DEBSIG_POLICY_DIR}/1password.pol'"
fi
if [[ ! -s "${DEBSIG_KEYRING_DIR}/debsig.gpg" ]]; then
run "curl -fsSL https://downloads.1password.com/linux/keys/1password.asc | gpg --dearmor -o '${DEBSIG_KEYRING_DIR}/debsig.gpg'"
fi
log "Installing 1Password..."
run "apt-get update -qq"
run "apt-get install -y 1password"
if (( ! DRY_RUN )) && command -v 1password >/dev/null 2>&1; then
log "1Password binary available at: $(command -v 1password)"
fi
log "Done."
| 1 | #!/usr/bin/env bash |
| 2 | # install-1password.sh — Install 1Password desktop from the official 1Password APT repo. |
| 3 | # Hardened: arch-aware, signed-by keyring, debsig verification dir, idempotent, --dry-run. |
| 4 | |
| 5 | set -euo pipefail |
| 6 | IFS=$'\n\t' |
| 7 | |
| 8 | readonly SCRIPT_NAME="${0##*/}" |
| 9 | DRY_RUN=0 |
| 10 | |
| 11 | usage() { |
| 12 | cat <<EOF |
| 13 | Usage: sudo $SCRIPT_NAME [--dry-run] [--help] |
| 14 | |
| 15 | Configures the official 1Password APT repository (with debsig policy and |
| 16 | keyring) and installs the 1Password desktop application. Idempotent: safe to |
| 17 | re-run. |
| 18 | |
| 19 | Options: |
| 20 | --dry-run Print actions without executing. |
| 21 | --help, -h Show this help. |
| 22 | EOF |
| 23 | } |
| 24 | |
| 25 | log() { printf '\033[1;34m[%s]\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*"; } |
| 26 | warn() { printf '\033[1;33m[%s] WARN:\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*" >&2; } |
| 27 | die() { printf '\033[1;31m[%s] ERROR:\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*" >&2; exit 1; } |
| 28 | run() { if (( DRY_RUN )); then printf ' DRY-RUN: %s\n' "$*"; else eval "$@"; fi; } |
| 29 | trap 'rc=$?; (( rc )) && printf "\033[1;31m[%s] failed at line %s (exit %d)\033[0m\n" "${SCRIPT_NAME%.sh}" "$LINENO" "$rc" >&2' ERR |
| 30 | |
| 31 | while (( $# )); do |
| 32 | case "$1" in |
| 33 | --dry-run) DRY_RUN=1 ;; |
| 34 | -h|--help) usage; exit 0 ;; |
| 35 | *) die "Unknown argument: $1 (try --help)" ;; |
| 36 | esac |
| 37 | shift |
| 38 | done |
| 39 | |
| 40 | (( EUID == 0 )) || die "Must run as root. Try: sudo $SCRIPT_NAME" |
| 41 | |
| 42 | [[ -r /etc/os-release ]] || die "/etc/os-release not found." |
| 43 | # shellcheck disable=SC1091 |
| 44 | . /etc/os-release |
| 45 | case "${ID:-}:${ID_LIKE:-}" in |
| 46 | *ubuntu*|*debian*) : ;; |
| 47 | *) die "Unsupported distro: ${PRETTY_NAME:-unknown}." ;; |
| 48 | esac |
| 49 | |
| 50 | # 1Password publishes builds for amd64 and arm64. |
| 51 | ARCH="$(dpkg --print-architecture)" |
| 52 | case "$ARCH" in |
| 53 | amd64|arm64) : ;; |
| 54 | *) die "1Password is only available for amd64/arm64 (detected: $ARCH)." ;; |
| 55 | esac |
| 56 | log "Detected: ${PRETTY_NAME:-unknown}, arch: $ARCH" |
| 57 | |
| 58 | export DEBIAN_FRONTEND=noninteractive |
| 59 | |
| 60 | KEYRING=/etc/apt/keyrings/1password-archive-keyring.gpg |
| 61 | SOURCES=/etc/apt/sources.list.d/1password.list |
| 62 | DEBSIG_POLICY_DIR=/etc/debsig/policies/AC2D62742012EA22 |
| 63 | DEBSIG_KEYRING_DIR=/usr/share/debsig/keyrings/AC2D62742012EA22 |
| 64 | |
| 65 | log "Installing prerequisites..." |
| 66 | run "apt-get update -qq" |
| 67 | run "apt-get install -y curl gpg ca-certificates" |
| 68 | |
| 69 | log "Configuring 1Password APT repository..." |
| 70 | run "install -d -m 0755 /etc/apt/keyrings" |
| 71 | if [[ ! -s "$KEYRING" ]]; then |
| 72 | run "curl -fsSL https://downloads.1password.com/linux/keys/1password.asc | gpg --dearmor -o '$KEYRING'" |
| 73 | run "chmod 0644 '$KEYRING'" |
| 74 | fi |
| 75 | |
| 76 | DESIRED_SRC="deb [arch=${ARCH} signed-by=${KEYRING}] https://downloads.1password.com/linux/debian/${ARCH} stable main" |
| 77 | if [[ ! -f "$SOURCES" ]] || ! grep -qxF "$DESIRED_SRC" "$SOURCES"; then |
| 78 | run "printf '%s\n' '$DESIRED_SRC' > '$SOURCES'" |
| 79 | fi |
| 80 | |
| 81 | log "Installing debsig policy (verifies package signatures on install)..." |
| 82 | run "install -d -m 0755 '$DEBSIG_POLICY_DIR' '$DEBSIG_KEYRING_DIR'" |
| 83 | if [[ ! -s "${DEBSIG_POLICY_DIR}/1password.pol" ]]; then |
| 84 | run "curl -fsSL https://downloads.1password.com/linux/debian/debsig/1password.pol -o '${DEBSIG_POLICY_DIR}/1password.pol'" |
| 85 | fi |
| 86 | if [[ ! -s "${DEBSIG_KEYRING_DIR}/debsig.gpg" ]]; then |
| 87 | run "curl -fsSL https://downloads.1password.com/linux/keys/1password.asc | gpg --dearmor -o '${DEBSIG_KEYRING_DIR}/debsig.gpg'" |
| 88 | fi |
| 89 | |
| 90 | log "Installing 1Password..." |
| 91 | run "apt-get update -qq" |
| 92 | run "apt-get install -y 1password" |
| 93 | |
| 94 | if (( ! DRY_RUN )) && command -v 1password >/dev/null 2>&1; then |
| 95 | log "1Password binary available at: $(command -v 1password)" |
| 96 | fi |
| 97 | log "Done." |
| 98 |
install-bruno.sh
· 3.4 KiB · Bash
Surowy
#!/usr/bin/env bash
# install-bruno.sh — Install Bruno API client from the official APT repository.
# Hardened: arch-aware, signed-by keyring with retries (keyserver flake), idempotent,
# clean desktop entry, --dry-run.
set -euo pipefail
IFS=$'\n\t'
readonly SCRIPT_NAME="${0##*/}"
DRY_RUN=0
usage() {
cat <<EOF
Usage: sudo $SCRIPT_NAME [--dry-run] [--help]
Configures the official Bruno APT repository (signed-by keyring fetched from
keyserver.ubuntu.com with retries) and installs Bruno. Idempotent: safe to
re-run.
Options:
--dry-run Print actions without executing.
--help, -h Show this help.
EOF
}
log() { printf '\033[1;34m[%s]\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*"; }
warn() { printf '\033[1;33m[%s] WARN:\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*" >&2; }
die() { printf '\033[1;31m[%s] ERROR:\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*" >&2; exit 1; }
run() { if (( DRY_RUN )); then printf ' DRY-RUN: %s\n' "$*"; else eval "$@"; fi; }
trap 'rc=$?; (( rc )) && printf "\033[1;31m[%s] failed at line %s (exit %d)\033[0m\n" "${SCRIPT_NAME%.sh}" "$LINENO" "$rc" >&2' ERR
while (( $# )); do
case "$1" in
--dry-run) DRY_RUN=1 ;;
-h|--help) usage; exit 0 ;;
*) die "Unknown argument: $1 (try --help)" ;;
esac
shift
done
(( EUID == 0 )) || die "Must run as root. Try: sudo $SCRIPT_NAME"
[[ -r /etc/os-release ]] || die "/etc/os-release not found."
# shellcheck disable=SC1091
. /etc/os-release
case "${ID:-}:${ID_LIKE:-}" in
*ubuntu*|*debian*) : ;;
*) die "Unsupported distro: ${PRETTY_NAME:-unknown}." ;;
esac
ARCH="$(dpkg --print-architecture)"
[[ "$ARCH" == "amd64" ]] || die "Bruno APT repo only ships amd64 (detected: $ARCH)."
log "Detected: ${PRETTY_NAME:-unknown}, arch: $ARCH"
export DEBIAN_FRONTEND=noninteractive
KEYRING=/etc/apt/keyrings/bruno.gpg
SOURCES=/etc/apt/sources.list.d/bruno.list
KEY_ID="9FA6017ECABE0266"
DESKTOP=/usr/share/applications/bruno.desktop
log "Installing prerequisites..."
run "apt-get update -qq"
run "apt-get install -y curl gpg ca-certificates"
log "Configuring Bruno APT repository..."
run "install -d -m 0755 /etc/apt/keyrings"
if [[ ! -s "$KEYRING" ]]; then
# keyserver.ubuntu.com is occasionally flaky — retry a few times.
ok=0
for attempt in 1 2 3 4 5; do
if (( DRY_RUN )); then
printf ' DRY-RUN: fetch key 0x%s (attempt %d)\n' "$KEY_ID" "$attempt"
ok=1
break
fi
if curl -fsSL --max-time 30 "https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x${KEY_ID}" \
| gpg --dearmor -o "$KEYRING" 2>/dev/null && [[ -s "$KEYRING" ]]; then
ok=1
break
fi
warn "Key fetch failed (attempt $attempt/5); retrying in $((attempt*2))s..."
sleep "$((attempt*2))"
done
(( ok )) || die "Could not retrieve Bruno signing key after 5 attempts."
run "chmod 0644 '$KEYRING'"
fi
DESIRED_SRC="deb [arch=${ARCH} signed-by=${KEYRING}] http://debian.usebruno.com/ bruno stable"
if [[ ! -f "$SOURCES" ]] || ! grep -qxF "$DESIRED_SRC" "$SOURCES"; then
run "printf '%s\n' '$DESIRED_SRC' > '$SOURCES'"
fi
log "Installing Bruno..."
run "apt-get update -qq"
run "apt-get install -y bruno"
log "Writing desktop entry..."
if (( DRY_RUN )); then
printf ' DRY-RUN: write %s\n' "$DESKTOP"
else
cat >"$DESKTOP" <<'EOF'
[Desktop Entry]
Name=Bruno
Comment=Open-source API Client
Exec=bruno %U
Terminal=false
Type=Application
Icon=bruno
Categories=Development;Utility;
StartupNotify=true
EOF
chmod 0644 "$DESKTOP"
fi
log "Done. Bruno installed."
| 1 | #!/usr/bin/env bash |
| 2 | # install-bruno.sh — Install Bruno API client from the official APT repository. |
| 3 | # Hardened: arch-aware, signed-by keyring with retries (keyserver flake), idempotent, |
| 4 | # clean desktop entry, --dry-run. |
| 5 | |
| 6 | set -euo pipefail |
| 7 | IFS=$'\n\t' |
| 8 | |
| 9 | readonly SCRIPT_NAME="${0##*/}" |
| 10 | DRY_RUN=0 |
| 11 | |
| 12 | usage() { |
| 13 | cat <<EOF |
| 14 | Usage: sudo $SCRIPT_NAME [--dry-run] [--help] |
| 15 | |
| 16 | Configures the official Bruno APT repository (signed-by keyring fetched from |
| 17 | keyserver.ubuntu.com with retries) and installs Bruno. Idempotent: safe to |
| 18 | re-run. |
| 19 | |
| 20 | Options: |
| 21 | --dry-run Print actions without executing. |
| 22 | --help, -h Show this help. |
| 23 | EOF |
| 24 | } |
| 25 | |
| 26 | log() { printf '\033[1;34m[%s]\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*"; } |
| 27 | warn() { printf '\033[1;33m[%s] WARN:\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*" >&2; } |
| 28 | die() { printf '\033[1;31m[%s] ERROR:\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*" >&2; exit 1; } |
| 29 | run() { if (( DRY_RUN )); then printf ' DRY-RUN: %s\n' "$*"; else eval "$@"; fi; } |
| 30 | trap 'rc=$?; (( rc )) && printf "\033[1;31m[%s] failed at line %s (exit %d)\033[0m\n" "${SCRIPT_NAME%.sh}" "$LINENO" "$rc" >&2' ERR |
| 31 | |
| 32 | while (( $# )); do |
| 33 | case "$1" in |
| 34 | --dry-run) DRY_RUN=1 ;; |
| 35 | -h|--help) usage; exit 0 ;; |
| 36 | *) die "Unknown argument: $1 (try --help)" ;; |
| 37 | esac |
| 38 | shift |
| 39 | done |
| 40 | |
| 41 | (( EUID == 0 )) || die "Must run as root. Try: sudo $SCRIPT_NAME" |
| 42 | |
| 43 | [[ -r /etc/os-release ]] || die "/etc/os-release not found." |
| 44 | # shellcheck disable=SC1091 |
| 45 | . /etc/os-release |
| 46 | case "${ID:-}:${ID_LIKE:-}" in |
| 47 | *ubuntu*|*debian*) : ;; |
| 48 | *) die "Unsupported distro: ${PRETTY_NAME:-unknown}." ;; |
| 49 | esac |
| 50 | |
| 51 | ARCH="$(dpkg --print-architecture)" |
| 52 | [[ "$ARCH" == "amd64" ]] || die "Bruno APT repo only ships amd64 (detected: $ARCH)." |
| 53 | log "Detected: ${PRETTY_NAME:-unknown}, arch: $ARCH" |
| 54 | |
| 55 | export DEBIAN_FRONTEND=noninteractive |
| 56 | |
| 57 | KEYRING=/etc/apt/keyrings/bruno.gpg |
| 58 | SOURCES=/etc/apt/sources.list.d/bruno.list |
| 59 | KEY_ID="9FA6017ECABE0266" |
| 60 | DESKTOP=/usr/share/applications/bruno.desktop |
| 61 | |
| 62 | log "Installing prerequisites..." |
| 63 | run "apt-get update -qq" |
| 64 | run "apt-get install -y curl gpg ca-certificates" |
| 65 | |
| 66 | log "Configuring Bruno APT repository..." |
| 67 | run "install -d -m 0755 /etc/apt/keyrings" |
| 68 | |
| 69 | if [[ ! -s "$KEYRING" ]]; then |
| 70 | # keyserver.ubuntu.com is occasionally flaky — retry a few times. |
| 71 | ok=0 |
| 72 | for attempt in 1 2 3 4 5; do |
| 73 | if (( DRY_RUN )); then |
| 74 | printf ' DRY-RUN: fetch key 0x%s (attempt %d)\n' "$KEY_ID" "$attempt" |
| 75 | ok=1 |
| 76 | break |
| 77 | fi |
| 78 | if curl -fsSL --max-time 30 "https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x${KEY_ID}" \ |
| 79 | | gpg --dearmor -o "$KEYRING" 2>/dev/null && [[ -s "$KEYRING" ]]; then |
| 80 | ok=1 |
| 81 | break |
| 82 | fi |
| 83 | warn "Key fetch failed (attempt $attempt/5); retrying in $((attempt*2))s..." |
| 84 | sleep "$((attempt*2))" |
| 85 | done |
| 86 | (( ok )) || die "Could not retrieve Bruno signing key after 5 attempts." |
| 87 | run "chmod 0644 '$KEYRING'" |
| 88 | fi |
| 89 | |
| 90 | DESIRED_SRC="deb [arch=${ARCH} signed-by=${KEYRING}] http://debian.usebruno.com/ bruno stable" |
| 91 | if [[ ! -f "$SOURCES" ]] || ! grep -qxF "$DESIRED_SRC" "$SOURCES"; then |
| 92 | run "printf '%s\n' '$DESIRED_SRC' > '$SOURCES'" |
| 93 | fi |
| 94 | |
| 95 | log "Installing Bruno..." |
| 96 | run "apt-get update -qq" |
| 97 | run "apt-get install -y bruno" |
| 98 | |
| 99 | log "Writing desktop entry..." |
| 100 | if (( DRY_RUN )); then |
| 101 | printf ' DRY-RUN: write %s\n' "$DESKTOP" |
| 102 | else |
| 103 | cat >"$DESKTOP" <<'EOF' |
| 104 | [Desktop Entry] |
| 105 | Name=Bruno |
| 106 | Comment=Open-source API Client |
| 107 | Exec=bruno %U |
| 108 | Terminal=false |
| 109 | Type=Application |
| 110 | Icon=bruno |
| 111 | Categories=Development;Utility; |
| 112 | StartupNotify=true |
| 113 | EOF |
| 114 | chmod 0644 "$DESKTOP" |
| 115 | fi |
| 116 | |
| 117 | log "Done. Bruno installed." |
| 118 |
install-espanso.sh
· 4.3 KiB · Bash
Surowy
#!/usr/bin/env bash
# install-espanso.sh — Install Espanso (text expander) with Wayland or X11 build.
# Auto-detects session type; falls back to X11 if Wayland is not active.
# Hardened: session detection, registers service as the invoking user, idempotent.
set -euo pipefail
IFS=$'\n\t'
readonly SCRIPT_NAME="${0##*/}"
DRY_RUN=0
FORCE_VARIANT="" # "wayland" | "x11"
usage() {
cat <<EOF
Usage: sudo $SCRIPT_NAME [--dry-run] [--wayland|--x11] [--help]
Installs Espanso from the latest GitHub release. Auto-detects whether to use
the Wayland or X11 build based on \$XDG_SESSION_TYPE of the invoking user
(SUDO_USER). The systemd-user service is registered for that user, not root.
Options:
--wayland Force the Wayland build.
--x11 Force the X11 build.
--dry-run Print actions without executing.
--help, -h Show this help.
EOF
}
log() { printf '\033[1;34m[%s]\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*"; }
warn() { printf '\033[1;33m[%s] WARN:\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*" >&2; }
die() { printf '\033[1;31m[%s] ERROR:\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*" >&2; exit 1; }
run() { if (( DRY_RUN )); then printf ' DRY-RUN: %s\n' "$*"; else eval "$@"; fi; }
trap 'rc=$?; (( rc )) && printf "\033[1;31m[%s] failed at line %s (exit %d)\033[0m\n" "${SCRIPT_NAME%.sh}" "$LINENO" "$rc" >&2' ERR
while (( $# )); do
case "$1" in
--dry-run) DRY_RUN=1 ;;
--wayland) FORCE_VARIANT=wayland ;;
--x11) FORCE_VARIANT=x11 ;;
-h|--help) usage; exit 0 ;;
*) die "Unknown argument: $1 (try --help)" ;;
esac
shift
done
(( EUID == 0 )) || die "Must run as root. Try: sudo $SCRIPT_NAME"
[[ -r /etc/os-release ]] || die "/etc/os-release not found."
# shellcheck disable=SC1091
. /etc/os-release
case "${ID:-}:${ID_LIKE:-}" in
*ubuntu*|*debian*) : ;;
*) die "Unsupported distro: ${PRETTY_NAME:-unknown}." ;;
esac
ARCH="$(dpkg --print-architecture)"
[[ "$ARCH" == "amd64" ]] || die "Espanso .deb is published for amd64 only (detected: $ARCH)."
ACTUAL_USER="${SUDO_USER:-${USER:-}}"
[[ -n "$ACTUAL_USER" && "$ACTUAL_USER" != "root" ]] || die "Run via sudo as a regular user; cannot register the service as root."
USER_ID="$(id -u "$ACTUAL_USER")"
# Detect session type from the invoking user's environment, fallback to env, fallback to wayland
if [[ -n "$FORCE_VARIANT" ]]; then
VARIANT="$FORCE_VARIANT"
else
SESSION_TYPE="$(sudo -u "$ACTUAL_USER" -i printenv XDG_SESSION_TYPE 2>/dev/null || true)"
[[ -z "$SESSION_TYPE" ]] && SESSION_TYPE="${XDG_SESSION_TYPE:-wayland}"
case "$SESSION_TYPE" in
wayland) VARIANT=wayland ;;
x11|tty) VARIANT=x11 ;;
*) warn "Unknown XDG_SESSION_TYPE='$SESSION_TYPE'; defaulting to wayland."; VARIANT=wayland ;;
esac
fi
log "Detected: ${PRETTY_NAME:-unknown}, user: $ACTUAL_USER, session: $VARIANT"
export DEBIAN_FRONTEND=noninteractive
TEMP_DEB="$(mktemp -t espanso.XXXXXX.deb)"
trap 'rm -f "$TEMP_DEB"' EXIT
DEB_NAME="espanso-debian-${VARIANT}-amd64.deb"
URL="https://github.com/espanso/espanso/releases/latest/download/${DEB_NAME}"
log "Installing prerequisites..."
run "apt-get update -qq"
run "apt-get install -y wget libcap2-bin"
log "Downloading ${DEB_NAME}..."
run "wget -qO '$TEMP_DEB' '$URL'"
log "Installing package..."
run "apt-get install -y '$TEMP_DEB'"
ESPANSO_BIN="$(command -v espanso || true)"
[[ -x "$ESPANSO_BIN" ]] || die "espanso binary not found after install."
if [[ "$VARIANT" == "wayland" ]]; then
log "Setting CAP_DAC_OVERRIDE on $ESPANSO_BIN..."
run "setcap 'cap_dac_override+p' '$ESPANSO_BIN'"
fi
log "Registering & starting espanso service for $ACTUAL_USER..."
# Register may fail if already registered — treat that as success.
if (( DRY_RUN )); then
printf ' DRY-RUN: sudo -u %s XDG_RUNTIME_DIR=/run/user/%s espanso service register || true\n' "$ACTUAL_USER" "$USER_ID"
printf ' DRY-RUN: sudo -u %s XDG_RUNTIME_DIR=/run/user/%s espanso start || true\n' "$ACTUAL_USER" "$USER_ID"
else
sudo -u "$ACTUAL_USER" XDG_RUNTIME_DIR="/run/user/$USER_ID" espanso service register || true
sudo -u "$ACTUAL_USER" XDG_RUNTIME_DIR="/run/user/$USER_ID" espanso restart || \
sudo -u "$ACTUAL_USER" XDG_RUNTIME_DIR="/run/user/$USER_ID" espanso start || true
fi
log "Done. Espanso ($VARIANT) installed and started for $ACTUAL_USER."
if [[ "$VARIANT" == "wayland" ]]; then
log "Wayland note: non-US keyboards must set the layout in ~/.config/espanso/config/default.yml"
fi
| 1 | #!/usr/bin/env bash |
| 2 | # install-espanso.sh — Install Espanso (text expander) with Wayland or X11 build. |
| 3 | # Auto-detects session type; falls back to X11 if Wayland is not active. |
| 4 | # Hardened: session detection, registers service as the invoking user, idempotent. |
| 5 | |
| 6 | set -euo pipefail |
| 7 | IFS=$'\n\t' |
| 8 | |
| 9 | readonly SCRIPT_NAME="${0##*/}" |
| 10 | DRY_RUN=0 |
| 11 | FORCE_VARIANT="" # "wayland" | "x11" |
| 12 | |
| 13 | usage() { |
| 14 | cat <<EOF |
| 15 | Usage: sudo $SCRIPT_NAME [--dry-run] [--wayland|--x11] [--help] |
| 16 | |
| 17 | Installs Espanso from the latest GitHub release. Auto-detects whether to use |
| 18 | the Wayland or X11 build based on \$XDG_SESSION_TYPE of the invoking user |
| 19 | (SUDO_USER). The systemd-user service is registered for that user, not root. |
| 20 | |
| 21 | Options: |
| 22 | --wayland Force the Wayland build. |
| 23 | --x11 Force the X11 build. |
| 24 | --dry-run Print actions without executing. |
| 25 | --help, -h Show this help. |
| 26 | EOF |
| 27 | } |
| 28 | |
| 29 | log() { printf '\033[1;34m[%s]\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*"; } |
| 30 | warn() { printf '\033[1;33m[%s] WARN:\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*" >&2; } |
| 31 | die() { printf '\033[1;31m[%s] ERROR:\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*" >&2; exit 1; } |
| 32 | run() { if (( DRY_RUN )); then printf ' DRY-RUN: %s\n' "$*"; else eval "$@"; fi; } |
| 33 | trap 'rc=$?; (( rc )) && printf "\033[1;31m[%s] failed at line %s (exit %d)\033[0m\n" "${SCRIPT_NAME%.sh}" "$LINENO" "$rc" >&2' ERR |
| 34 | |
| 35 | while (( $# )); do |
| 36 | case "$1" in |
| 37 | --dry-run) DRY_RUN=1 ;; |
| 38 | --wayland) FORCE_VARIANT=wayland ;; |
| 39 | --x11) FORCE_VARIANT=x11 ;; |
| 40 | -h|--help) usage; exit 0 ;; |
| 41 | *) die "Unknown argument: $1 (try --help)" ;; |
| 42 | esac |
| 43 | shift |
| 44 | done |
| 45 | |
| 46 | (( EUID == 0 )) || die "Must run as root. Try: sudo $SCRIPT_NAME" |
| 47 | |
| 48 | [[ -r /etc/os-release ]] || die "/etc/os-release not found." |
| 49 | # shellcheck disable=SC1091 |
| 50 | . /etc/os-release |
| 51 | case "${ID:-}:${ID_LIKE:-}" in |
| 52 | *ubuntu*|*debian*) : ;; |
| 53 | *) die "Unsupported distro: ${PRETTY_NAME:-unknown}." ;; |
| 54 | esac |
| 55 | |
| 56 | ARCH="$(dpkg --print-architecture)" |
| 57 | [[ "$ARCH" == "amd64" ]] || die "Espanso .deb is published for amd64 only (detected: $ARCH)." |
| 58 | |
| 59 | ACTUAL_USER="${SUDO_USER:-${USER:-}}" |
| 60 | [[ -n "$ACTUAL_USER" && "$ACTUAL_USER" != "root" ]] || die "Run via sudo as a regular user; cannot register the service as root." |
| 61 | USER_ID="$(id -u "$ACTUAL_USER")" |
| 62 | |
| 63 | # Detect session type from the invoking user's environment, fallback to env, fallback to wayland |
| 64 | if [[ -n "$FORCE_VARIANT" ]]; then |
| 65 | VARIANT="$FORCE_VARIANT" |
| 66 | else |
| 67 | SESSION_TYPE="$(sudo -u "$ACTUAL_USER" -i printenv XDG_SESSION_TYPE 2>/dev/null || true)" |
| 68 | [[ -z "$SESSION_TYPE" ]] && SESSION_TYPE="${XDG_SESSION_TYPE:-wayland}" |
| 69 | case "$SESSION_TYPE" in |
| 70 | wayland) VARIANT=wayland ;; |
| 71 | x11|tty) VARIANT=x11 ;; |
| 72 | *) warn "Unknown XDG_SESSION_TYPE='$SESSION_TYPE'; defaulting to wayland."; VARIANT=wayland ;; |
| 73 | esac |
| 74 | fi |
| 75 | log "Detected: ${PRETTY_NAME:-unknown}, user: $ACTUAL_USER, session: $VARIANT" |
| 76 | |
| 77 | export DEBIAN_FRONTEND=noninteractive |
| 78 | TEMP_DEB="$(mktemp -t espanso.XXXXXX.deb)" |
| 79 | trap 'rm -f "$TEMP_DEB"' EXIT |
| 80 | |
| 81 | DEB_NAME="espanso-debian-${VARIANT}-amd64.deb" |
| 82 | URL="https://github.com/espanso/espanso/releases/latest/download/${DEB_NAME}" |
| 83 | |
| 84 | log "Installing prerequisites..." |
| 85 | run "apt-get update -qq" |
| 86 | run "apt-get install -y wget libcap2-bin" |
| 87 | |
| 88 | log "Downloading ${DEB_NAME}..." |
| 89 | run "wget -qO '$TEMP_DEB' '$URL'" |
| 90 | |
| 91 | log "Installing package..." |
| 92 | run "apt-get install -y '$TEMP_DEB'" |
| 93 | |
| 94 | ESPANSO_BIN="$(command -v espanso || true)" |
| 95 | [[ -x "$ESPANSO_BIN" ]] || die "espanso binary not found after install." |
| 96 | |
| 97 | if [[ "$VARIANT" == "wayland" ]]; then |
| 98 | log "Setting CAP_DAC_OVERRIDE on $ESPANSO_BIN..." |
| 99 | run "setcap 'cap_dac_override+p' '$ESPANSO_BIN'" |
| 100 | fi |
| 101 | |
| 102 | log "Registering & starting espanso service for $ACTUAL_USER..." |
| 103 | # Register may fail if already registered — treat that as success. |
| 104 | if (( DRY_RUN )); then |
| 105 | printf ' DRY-RUN: sudo -u %s XDG_RUNTIME_DIR=/run/user/%s espanso service register || true\n' "$ACTUAL_USER" "$USER_ID" |
| 106 | printf ' DRY-RUN: sudo -u %s XDG_RUNTIME_DIR=/run/user/%s espanso start || true\n' "$ACTUAL_USER" "$USER_ID" |
| 107 | else |
| 108 | sudo -u "$ACTUAL_USER" XDG_RUNTIME_DIR="/run/user/$USER_ID" espanso service register || true |
| 109 | sudo -u "$ACTUAL_USER" XDG_RUNTIME_DIR="/run/user/$USER_ID" espanso restart || \ |
| 110 | sudo -u "$ACTUAL_USER" XDG_RUNTIME_DIR="/run/user/$USER_ID" espanso start || true |
| 111 | fi |
| 112 | |
| 113 | log "Done. Espanso ($VARIANT) installed and started for $ACTUAL_USER." |
| 114 | if [[ "$VARIANT" == "wayland" ]]; then |
| 115 | log "Wayland note: non-US keyboards must set the layout in ~/.config/espanso/config/default.yml" |
| 116 | fi |
| 117 |
install-firefox.sh
· 4.2 KiB · Bash
Surowy
#!/usr/bin/env bash
# install-firefox.sh — Install Firefox from Mozilla's official APT repository.
# Removes the Snap transition package and pins Mozilla as the source of truth.
# Hardened: distro-detect, idempotent, signed-by keyring, ERR trap, --dry-run.
set -euo pipefail
IFS=$'\n\t'
readonly SCRIPT_NAME="${0##*/}"
DRY_RUN=0
ASSUME_YES=0
usage() {
cat <<EOF
Usage: sudo $SCRIPT_NAME [--dry-run] [--yes] [--help]
Removes Firefox Snap (and Ubuntu's transitional wrapper), configures the
official Mozilla APT repository (signed-by keyring + APT pin), then installs
Firefox so it auto-updates from Mozilla.
Options:
--dry-run Print the actions without executing them.
--yes, -y Pass -y to apt for non-interactive install.
--help, -h Show this help.
EOF
}
log() { printf '\033[1;34m[%s]\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*"; }
warn() { printf '\033[1;33m[%s] WARN:\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*" >&2; }
die() { printf '\033[1;31m[%s] ERROR:\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*" >&2; exit 1; }
run() { if (( DRY_RUN )); then printf ' DRY-RUN: %s\n' "$*"; else eval "$@"; fi; }
on_err() { local rc=$? line=$1; printf '\033[1;31m[%s] failed at line %s (exit %d)\033[0m\n' "${SCRIPT_NAME%.sh}" "$line" "$rc" >&2; }
trap 'on_err $LINENO' ERR
while (( $# )); do
case "$1" in
--dry-run) DRY_RUN=1 ;;
-y|--yes) ASSUME_YES=1 ;;
-h|--help) usage; exit 0 ;;
*) die "Unknown argument: $1 (try --help)" ;;
esac
shift
done
(( EUID == 0 )) || die "Must run as root. Try: sudo $SCRIPT_NAME"
[[ -r /etc/os-release ]] || die "/etc/os-release not found; cannot detect distro."
# shellcheck disable=SC1091
. /etc/os-release
case "${ID:-}:${ID_LIKE:-}" in
*ubuntu*|*debian*) : ;;
*) die "Unsupported distro: ${PRETTY_NAME:-unknown}. Requires Debian/Ubuntu." ;;
esac
log "Detected: ${PRETTY_NAME:-unknown} (codename: ${VERSION_CODENAME:-?})"
APT_YES=()
(( ASSUME_YES )) && APT_YES=(-y) || APT_YES=(-y) # always -y for safety in scripted use
export DEBIAN_FRONTEND=noninteractive
KEYRING=/etc/apt/keyrings/packages.mozilla.org.asc
SOURCES=/etc/apt/sources.list.d/mozilla.list
PIN=/etc/apt/preferences.d/mozilla
log "Removing Firefox Snap and Ubuntu's transitional wrapper (if present)..."
if command -v snap >/dev/null 2>&1; then
run "snap disable firefox >/dev/null 2>&1 || true"
run "snap remove --purge firefox >/dev/null 2>&1 || true"
fi
run "apt-get remove --purge ${APT_YES[*]} firefox >/dev/null 2>&1 || true"
run "rm -f /usr/bin/firefox"
log "Installing prerequisites (wget, gpg, ca-certificates)..."
run "apt-get update -qq"
run "apt-get install ${APT_YES[*]} wget gpg ca-certificates"
log "Configuring Mozilla APT repository..."
run "install -d -m 0755 /etc/apt/keyrings"
if [[ ! -s "$KEYRING" ]]; then
run "wget -qO '$KEYRING' https://packages.mozilla.org/apt/repo-signing-key.gpg"
run "chmod 0644 '$KEYRING'"
else
log "Keyring already present at $KEYRING (skipping download)."
fi
# Verify key fingerprint matches Mozilla's published fingerprint
EXPECTED_FPR="35BAA0B33E9EB396F59CA838C0BA5CE6DC6315A3"
ACTUAL_FPR="$(gpg --show-keys --with-colons "$KEYRING" 2>/dev/null | awk -F: '/^fpr/ {print $10; exit}')"
if [[ "$ACTUAL_FPR" != "$EXPECTED_FPR" ]]; then
warn "Mozilla key fingerprint mismatch (expected $EXPECTED_FPR, got ${ACTUAL_FPR:-none}). Continuing, but verify manually."
else
log "Mozilla key fingerprint verified."
fi
DESIRED_SRC='deb [signed-by=/etc/apt/keyrings/packages.mozilla.org.asc] https://packages.mozilla.org/apt mozilla main'
if [[ ! -f "$SOURCES" ]] || ! grep -qxF "$DESIRED_SRC" "$SOURCES"; then
run "printf '%s\n' '$DESIRED_SRC' > '$SOURCES'"
fi
log "Pinning Mozilla repo to priority 1000..."
if [[ ! -f "$PIN" ]] || ! grep -q 'origin packages.mozilla.org' "$PIN"; then
if (( DRY_RUN )); then
printf ' DRY-RUN: write %s\n' "$PIN"
else
cat >"$PIN" <<'EOF'
Package: *
Pin: origin packages.mozilla.org
Pin-Priority: 1000
EOF
fi
fi
log "Installing Firefox from Mozilla repo..."
run "apt-get update -qq"
run "apt-get install ${APT_YES[*]} firefox"
if (( ! DRY_RUN )) && command -v firefox >/dev/null 2>&1; then
log "Installed: $(firefox --version 2>/dev/null || echo 'firefox')"
fi
log "Done. Firefox installed from Mozilla APT and will auto-update."
| 1 | #!/usr/bin/env bash |
| 2 | # install-firefox.sh — Install Firefox from Mozilla's official APT repository. |
| 3 | # Removes the Snap transition package and pins Mozilla as the source of truth. |
| 4 | # Hardened: distro-detect, idempotent, signed-by keyring, ERR trap, --dry-run. |
| 5 | |
| 6 | set -euo pipefail |
| 7 | IFS=$'\n\t' |
| 8 | |
| 9 | readonly SCRIPT_NAME="${0##*/}" |
| 10 | DRY_RUN=0 |
| 11 | ASSUME_YES=0 |
| 12 | |
| 13 | usage() { |
| 14 | cat <<EOF |
| 15 | Usage: sudo $SCRIPT_NAME [--dry-run] [--yes] [--help] |
| 16 | |
| 17 | Removes Firefox Snap (and Ubuntu's transitional wrapper), configures the |
| 18 | official Mozilla APT repository (signed-by keyring + APT pin), then installs |
| 19 | Firefox so it auto-updates from Mozilla. |
| 20 | |
| 21 | Options: |
| 22 | --dry-run Print the actions without executing them. |
| 23 | --yes, -y Pass -y to apt for non-interactive install. |
| 24 | --help, -h Show this help. |
| 25 | EOF |
| 26 | } |
| 27 | |
| 28 | log() { printf '\033[1;34m[%s]\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*"; } |
| 29 | warn() { printf '\033[1;33m[%s] WARN:\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*" >&2; } |
| 30 | die() { printf '\033[1;31m[%s] ERROR:\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*" >&2; exit 1; } |
| 31 | run() { if (( DRY_RUN )); then printf ' DRY-RUN: %s\n' "$*"; else eval "$@"; fi; } |
| 32 | |
| 33 | on_err() { local rc=$? line=$1; printf '\033[1;31m[%s] failed at line %s (exit %d)\033[0m\n' "${SCRIPT_NAME%.sh}" "$line" "$rc" >&2; } |
| 34 | trap 'on_err $LINENO' ERR |
| 35 | |
| 36 | while (( $# )); do |
| 37 | case "$1" in |
| 38 | --dry-run) DRY_RUN=1 ;; |
| 39 | -y|--yes) ASSUME_YES=1 ;; |
| 40 | -h|--help) usage; exit 0 ;; |
| 41 | *) die "Unknown argument: $1 (try --help)" ;; |
| 42 | esac |
| 43 | shift |
| 44 | done |
| 45 | |
| 46 | (( EUID == 0 )) || die "Must run as root. Try: sudo $SCRIPT_NAME" |
| 47 | |
| 48 | [[ -r /etc/os-release ]] || die "/etc/os-release not found; cannot detect distro." |
| 49 | # shellcheck disable=SC1091 |
| 50 | . /etc/os-release |
| 51 | case "${ID:-}:${ID_LIKE:-}" in |
| 52 | *ubuntu*|*debian*) : ;; |
| 53 | *) die "Unsupported distro: ${PRETTY_NAME:-unknown}. Requires Debian/Ubuntu." ;; |
| 54 | esac |
| 55 | log "Detected: ${PRETTY_NAME:-unknown} (codename: ${VERSION_CODENAME:-?})" |
| 56 | |
| 57 | APT_YES=() |
| 58 | (( ASSUME_YES )) && APT_YES=(-y) || APT_YES=(-y) # always -y for safety in scripted use |
| 59 | export DEBIAN_FRONTEND=noninteractive |
| 60 | |
| 61 | KEYRING=/etc/apt/keyrings/packages.mozilla.org.asc |
| 62 | SOURCES=/etc/apt/sources.list.d/mozilla.list |
| 63 | PIN=/etc/apt/preferences.d/mozilla |
| 64 | |
| 65 | log "Removing Firefox Snap and Ubuntu's transitional wrapper (if present)..." |
| 66 | if command -v snap >/dev/null 2>&1; then |
| 67 | run "snap disable firefox >/dev/null 2>&1 || true" |
| 68 | run "snap remove --purge firefox >/dev/null 2>&1 || true" |
| 69 | fi |
| 70 | run "apt-get remove --purge ${APT_YES[*]} firefox >/dev/null 2>&1 || true" |
| 71 | run "rm -f /usr/bin/firefox" |
| 72 | |
| 73 | log "Installing prerequisites (wget, gpg, ca-certificates)..." |
| 74 | run "apt-get update -qq" |
| 75 | run "apt-get install ${APT_YES[*]} wget gpg ca-certificates" |
| 76 | |
| 77 | log "Configuring Mozilla APT repository..." |
| 78 | run "install -d -m 0755 /etc/apt/keyrings" |
| 79 | if [[ ! -s "$KEYRING" ]]; then |
| 80 | run "wget -qO '$KEYRING' https://packages.mozilla.org/apt/repo-signing-key.gpg" |
| 81 | run "chmod 0644 '$KEYRING'" |
| 82 | else |
| 83 | log "Keyring already present at $KEYRING (skipping download)." |
| 84 | fi |
| 85 | |
| 86 | # Verify key fingerprint matches Mozilla's published fingerprint |
| 87 | EXPECTED_FPR="35BAA0B33E9EB396F59CA838C0BA5CE6DC6315A3" |
| 88 | ACTUAL_FPR="$(gpg --show-keys --with-colons "$KEYRING" 2>/dev/null | awk -F: '/^fpr/ {print $10; exit}')" |
| 89 | if [[ "$ACTUAL_FPR" != "$EXPECTED_FPR" ]]; then |
| 90 | warn "Mozilla key fingerprint mismatch (expected $EXPECTED_FPR, got ${ACTUAL_FPR:-none}). Continuing, but verify manually." |
| 91 | else |
| 92 | log "Mozilla key fingerprint verified." |
| 93 | fi |
| 94 | |
| 95 | DESIRED_SRC='deb [signed-by=/etc/apt/keyrings/packages.mozilla.org.asc] https://packages.mozilla.org/apt mozilla main' |
| 96 | if [[ ! -f "$SOURCES" ]] || ! grep -qxF "$DESIRED_SRC" "$SOURCES"; then |
| 97 | run "printf '%s\n' '$DESIRED_SRC' > '$SOURCES'" |
| 98 | fi |
| 99 | |
| 100 | log "Pinning Mozilla repo to priority 1000..." |
| 101 | if [[ ! -f "$PIN" ]] || ! grep -q 'origin packages.mozilla.org' "$PIN"; then |
| 102 | if (( DRY_RUN )); then |
| 103 | printf ' DRY-RUN: write %s\n' "$PIN" |
| 104 | else |
| 105 | cat >"$PIN" <<'EOF' |
| 106 | Package: * |
| 107 | Pin: origin packages.mozilla.org |
| 108 | Pin-Priority: 1000 |
| 109 | EOF |
| 110 | fi |
| 111 | fi |
| 112 | |
| 113 | log "Installing Firefox from Mozilla repo..." |
| 114 | run "apt-get update -qq" |
| 115 | run "apt-get install ${APT_YES[*]} firefox" |
| 116 | |
| 117 | if (( ! DRY_RUN )) && command -v firefox >/dev/null 2>&1; then |
| 118 | log "Installed: $(firefox --version 2>/dev/null || echo 'firefox')" |
| 119 | fi |
| 120 | |
| 121 | log "Done. Firefox installed from Mozilla APT and will auto-update." |
| 122 |
install-ipatool.sh
· 4.3 KiB · Bash
Surowy
#!/usr/bin/env bash
# install-ipatool.sh — Install ipatool from the latest GitHub release.
# Hardened: arch detection (amd64/arm64), GitHub API token support, SHA-256 verification
# from the release checksum file, atomic install to /usr/local/bin, --dry-run.
set -euo pipefail
IFS=$'\n\t'
readonly SCRIPT_NAME="${0##*/}"
DRY_RUN=0
REPO="majd/ipatool"
INSTALL_PATH="/usr/local/bin/ipatool"
usage() {
cat <<EOF
Usage: sudo $SCRIPT_NAME [--dry-run] [--help]
Resolves the latest release of majd/ipatool, downloads the tarball for your
architecture, verifies its SHA-256 against the published checksums.txt, and
installs the binary to /usr/local/bin/ipatool atomically.
If \$GITHUB_TOKEN is set in the environment, it is used to authenticate the
GitHub API request (avoids rate limits).
Options:
--dry-run Print actions without executing.
--help, -h Show this help.
EOF
}
log() { printf '\033[1;34m[%s]\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*"; }
warn() { printf '\033[1;33m[%s] WARN:\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*" >&2; }
die() { printf '\033[1;31m[%s] ERROR:\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*" >&2; exit 1; }
run() { if (( DRY_RUN )); then printf ' DRY-RUN: %s\n' "$*"; else eval "$@"; fi; }
trap 'rc=$?; (( rc )) && printf "\033[1;31m[%s] failed at line %s (exit %d)\033[0m\n" "${SCRIPT_NAME%.sh}" "$LINENO" "$rc" >&2' ERR
while (( $# )); do
case "$1" in
--dry-run) DRY_RUN=1 ;;
-h|--help) usage; exit 0 ;;
*) die "Unknown argument: $1 (try --help)" ;;
esac
shift
done
(( EUID == 0 )) || die "Must run as root. Try: sudo $SCRIPT_NAME"
[[ -r /etc/os-release ]] || die "/etc/os-release not found."
# shellcheck disable=SC1091
. /etc/os-release
log "Detected: ${PRETTY_NAME:-unknown}"
ARCH_RAW="$(uname -m)"
case "$ARCH_RAW" in
x86_64) GO_ARCH=amd64 ;;
aarch64) GO_ARCH=arm64 ;;
armv7l|armv6l) GO_ARCH=arm ;;
*) die "Unsupported architecture: $ARCH_RAW" ;;
esac
ASSET_SUFFIX="linux-${GO_ARCH}.tar.gz"
export DEBIAN_FRONTEND=noninteractive
log "Installing prerequisites..."
run "apt-get update -qq"
run "apt-get install -y curl jq tar ca-certificates libsecret-1-0"
GH_HDRS=(-H "Accept: application/vnd.github+json")
[[ -n "${GITHUB_TOKEN:-}" ]] && GH_HDRS+=(-H "Authorization: Bearer ${GITHUB_TOKEN}")
log "Querying GitHub API for latest release of $REPO..."
RELEASE_JSON="$(curl -fsSL "${GH_HDRS[@]}" "https://api.github.com/repos/${REPO}/releases/latest")"
TAG="$(jq -r '.tag_name // empty' <<<"$RELEASE_JSON")"
[[ -n "$TAG" ]] || die "Could not parse latest release tag (rate limited? set GITHUB_TOKEN)."
log "Latest release: $TAG"
DOWNLOAD_URL="$(jq -r --arg s "$ASSET_SUFFIX" '.assets[] | select(.name | endswith($s)) | .browser_download_url' <<<"$RELEASE_JSON" | head -n1)"
CHECKSUM_URL="$(jq -r '.assets[] | select(.name | test("checksums?\\.txt$")) | .browser_download_url' <<<"$RELEASE_JSON" | head -n1)"
[[ "$DOWNLOAD_URL" =~ ^https:// ]] || die "No release asset matching '*${ASSET_SUFFIX}'."
STAGE="$(mktemp -d -t ipatool.XXXXXX)"
trap 'rm -rf "$STAGE"' EXIT
TARBALL="$STAGE/ipatool.tar.gz"
log "Downloading $(basename "$DOWNLOAD_URL")..."
run "curl -fsSL -o '$TARBALL' '$DOWNLOAD_URL'"
if [[ -n "$CHECKSUM_URL" ]]; then
log "Verifying SHA-256..."
EXPECTED="$(curl -fsSL "$CHECKSUM_URL" | awk -v f="$(basename "$DOWNLOAD_URL")" '$2 ~ f || $2 == "*"f {print $1; exit}')"
ACTUAL="$(sha256sum "$TARBALL" | awk '{print $1}')"
if [[ -n "$EXPECTED" && "$EXPECTED" != "$ACTUAL" ]]; then
die "SHA-256 mismatch: expected=$EXPECTED actual=$ACTUAL"
fi
[[ -n "$EXPECTED" ]] && log "SHA-256 ok." || warn "Asset not listed in checksums.txt; skipping."
else
warn "No checksums.txt in release; skipping SHA-256 verification."
fi
log "Extracting..."
run "tar -xzf '$TARBALL' -C '$STAGE'"
BINARY_PATH="$(find "$STAGE" -type f -name ipatool -executable -not -name '*.tar.gz' | head -n1 || true)"
if [[ -z "$BINARY_PATH" ]]; then
# Some releases ship the binary without +x; relax the find
BINARY_PATH="$(find "$STAGE" -type f -name ipatool -not -name '*.tar.gz' | head -n1 || true)"
fi
[[ -n "$BINARY_PATH" || $DRY_RUN -eq 1 ]] || die "ipatool binary not found inside archive."
log "Installing to $INSTALL_PATH..."
run "install -m 0755 '$BINARY_PATH' '$INSTALL_PATH'"
if (( ! DRY_RUN )) && command -v ipatool >/dev/null 2>&1; then
log "Installed: $(ipatool --version 2>/dev/null || basename "$INSTALL_PATH") ($TAG)"
fi
log "Done."
| 1 | #!/usr/bin/env bash |
| 2 | # install-ipatool.sh — Install ipatool from the latest GitHub release. |
| 3 | # Hardened: arch detection (amd64/arm64), GitHub API token support, SHA-256 verification |
| 4 | # from the release checksum file, atomic install to /usr/local/bin, --dry-run. |
| 5 | |
| 6 | set -euo pipefail |
| 7 | IFS=$'\n\t' |
| 8 | |
| 9 | readonly SCRIPT_NAME="${0##*/}" |
| 10 | DRY_RUN=0 |
| 11 | REPO="majd/ipatool" |
| 12 | INSTALL_PATH="/usr/local/bin/ipatool" |
| 13 | |
| 14 | usage() { |
| 15 | cat <<EOF |
| 16 | Usage: sudo $SCRIPT_NAME [--dry-run] [--help] |
| 17 | |
| 18 | Resolves the latest release of majd/ipatool, downloads the tarball for your |
| 19 | architecture, verifies its SHA-256 against the published checksums.txt, and |
| 20 | installs the binary to /usr/local/bin/ipatool atomically. |
| 21 | |
| 22 | If \$GITHUB_TOKEN is set in the environment, it is used to authenticate the |
| 23 | GitHub API request (avoids rate limits). |
| 24 | |
| 25 | Options: |
| 26 | --dry-run Print actions without executing. |
| 27 | --help, -h Show this help. |
| 28 | EOF |
| 29 | } |
| 30 | |
| 31 | log() { printf '\033[1;34m[%s]\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*"; } |
| 32 | warn() { printf '\033[1;33m[%s] WARN:\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*" >&2; } |
| 33 | die() { printf '\033[1;31m[%s] ERROR:\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*" >&2; exit 1; } |
| 34 | run() { if (( DRY_RUN )); then printf ' DRY-RUN: %s\n' "$*"; else eval "$@"; fi; } |
| 35 | trap 'rc=$?; (( rc )) && printf "\033[1;31m[%s] failed at line %s (exit %d)\033[0m\n" "${SCRIPT_NAME%.sh}" "$LINENO" "$rc" >&2' ERR |
| 36 | |
| 37 | while (( $# )); do |
| 38 | case "$1" in |
| 39 | --dry-run) DRY_RUN=1 ;; |
| 40 | -h|--help) usage; exit 0 ;; |
| 41 | *) die "Unknown argument: $1 (try --help)" ;; |
| 42 | esac |
| 43 | shift |
| 44 | done |
| 45 | |
| 46 | (( EUID == 0 )) || die "Must run as root. Try: sudo $SCRIPT_NAME" |
| 47 | |
| 48 | [[ -r /etc/os-release ]] || die "/etc/os-release not found." |
| 49 | # shellcheck disable=SC1091 |
| 50 | . /etc/os-release |
| 51 | log "Detected: ${PRETTY_NAME:-unknown}" |
| 52 | |
| 53 | ARCH_RAW="$(uname -m)" |
| 54 | case "$ARCH_RAW" in |
| 55 | x86_64) GO_ARCH=amd64 ;; |
| 56 | aarch64) GO_ARCH=arm64 ;; |
| 57 | armv7l|armv6l) GO_ARCH=arm ;; |
| 58 | *) die "Unsupported architecture: $ARCH_RAW" ;; |
| 59 | esac |
| 60 | ASSET_SUFFIX="linux-${GO_ARCH}.tar.gz" |
| 61 | |
| 62 | export DEBIAN_FRONTEND=noninteractive |
| 63 | log "Installing prerequisites..." |
| 64 | run "apt-get update -qq" |
| 65 | run "apt-get install -y curl jq tar ca-certificates libsecret-1-0" |
| 66 | |
| 67 | GH_HDRS=(-H "Accept: application/vnd.github+json") |
| 68 | [[ -n "${GITHUB_TOKEN:-}" ]] && GH_HDRS+=(-H "Authorization: Bearer ${GITHUB_TOKEN}") |
| 69 | |
| 70 | log "Querying GitHub API for latest release of $REPO..." |
| 71 | RELEASE_JSON="$(curl -fsSL "${GH_HDRS[@]}" "https://api.github.com/repos/${REPO}/releases/latest")" |
| 72 | TAG="$(jq -r '.tag_name // empty' <<<"$RELEASE_JSON")" |
| 73 | [[ -n "$TAG" ]] || die "Could not parse latest release tag (rate limited? set GITHUB_TOKEN)." |
| 74 | log "Latest release: $TAG" |
| 75 | |
| 76 | DOWNLOAD_URL="$(jq -r --arg s "$ASSET_SUFFIX" '.assets[] | select(.name | endswith($s)) | .browser_download_url' <<<"$RELEASE_JSON" | head -n1)" |
| 77 | CHECKSUM_URL="$(jq -r '.assets[] | select(.name | test("checksums?\\.txt$")) | .browser_download_url' <<<"$RELEASE_JSON" | head -n1)" |
| 78 | [[ "$DOWNLOAD_URL" =~ ^https:// ]] || die "No release asset matching '*${ASSET_SUFFIX}'." |
| 79 | |
| 80 | STAGE="$(mktemp -d -t ipatool.XXXXXX)" |
| 81 | trap 'rm -rf "$STAGE"' EXIT |
| 82 | TARBALL="$STAGE/ipatool.tar.gz" |
| 83 | |
| 84 | log "Downloading $(basename "$DOWNLOAD_URL")..." |
| 85 | run "curl -fsSL -o '$TARBALL' '$DOWNLOAD_URL'" |
| 86 | |
| 87 | if [[ -n "$CHECKSUM_URL" ]]; then |
| 88 | log "Verifying SHA-256..." |
| 89 | EXPECTED="$(curl -fsSL "$CHECKSUM_URL" | awk -v f="$(basename "$DOWNLOAD_URL")" '$2 ~ f || $2 == "*"f {print $1; exit}')" |
| 90 | ACTUAL="$(sha256sum "$TARBALL" | awk '{print $1}')" |
| 91 | if [[ -n "$EXPECTED" && "$EXPECTED" != "$ACTUAL" ]]; then |
| 92 | die "SHA-256 mismatch: expected=$EXPECTED actual=$ACTUAL" |
| 93 | fi |
| 94 | [[ -n "$EXPECTED" ]] && log "SHA-256 ok." || warn "Asset not listed in checksums.txt; skipping." |
| 95 | else |
| 96 | warn "No checksums.txt in release; skipping SHA-256 verification." |
| 97 | fi |
| 98 | |
| 99 | log "Extracting..." |
| 100 | run "tar -xzf '$TARBALL' -C '$STAGE'" |
| 101 | BINARY_PATH="$(find "$STAGE" -type f -name ipatool -executable -not -name '*.tar.gz' | head -n1 || true)" |
| 102 | if [[ -z "$BINARY_PATH" ]]; then |
| 103 | # Some releases ship the binary without +x; relax the find |
| 104 | BINARY_PATH="$(find "$STAGE" -type f -name ipatool -not -name '*.tar.gz' | head -n1 || true)" |
| 105 | fi |
| 106 | [[ -n "$BINARY_PATH" || $DRY_RUN -eq 1 ]] || die "ipatool binary not found inside archive." |
| 107 | |
| 108 | log "Installing to $INSTALL_PATH..." |
| 109 | run "install -m 0755 '$BINARY_PATH' '$INSTALL_PATH'" |
| 110 | |
| 111 | if (( ! DRY_RUN )) && command -v ipatool >/dev/null 2>&1; then |
| 112 | log "Installed: $(ipatool --version 2>/dev/null || basename "$INSTALL_PATH") ($TAG)" |
| 113 | fi |
| 114 | log "Done." |
| 115 |
install-jetbrains-toolbox.sh
· 5.5 KiB · Bash
Surowy
#!/usr/bin/env bash
# install-jetbrains-toolbox.sh — Install JetBrains Toolbox into the invoking user's home.
# Hardened: dependency check w/ auto-install, SHA-256 verification against JetBrains'
# release metadata, idempotent (replaces atomically), .desktop entry, --dry-run.
set -euo pipefail
IFS=$'\n\t'
readonly SCRIPT_NAME="${0##*/}"
DRY_RUN=0
ASSUME_YES=0
usage() {
cat <<EOF
Usage: $SCRIPT_NAME [--dry-run] [--yes] [--help]
Installs the latest JetBrains Toolbox to ~/.local/share/JetBrains/Toolbox and
creates a .desktop launcher in ~/.local/share/applications. The .tar.gz is
verified against the SHA-256 published in JetBrains' release feed.
Do NOT run with sudo: Toolbox is a per-user install. If a missing system
package (curl/jq/tar/libfuse2) needs installing, the script will call sudo
just for that step.
Options:
--yes, -y Auto-confirm prompts for installing missing system packages.
--dry-run Print actions without executing.
--help, -h Show this help.
EOF
}
log() { printf '\033[1;34m[%s]\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*"; }
warn() { printf '\033[1;33m[%s] WARN:\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*" >&2; }
die() { printf '\033[1;31m[%s] ERROR:\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*" >&2; exit 1; }
run() { if (( DRY_RUN )); then printf ' DRY-RUN: %s\n' "$*"; else eval "$@"; fi; }
trap 'rc=$?; (( rc )) && printf "\033[1;31m[%s] failed at line %s (exit %d)\033[0m\n" "${SCRIPT_NAME%.sh}" "$LINENO" "$rc" >&2' ERR
while (( $# )); do
case "$1" in
--dry-run) DRY_RUN=1 ;;
-y|--yes) ASSUME_YES=1 ;;
-h|--help) usage; exit 0 ;;
*) die "Unknown argument: $1 (try --help)" ;;
esac
shift
done
(( EUID != 0 )) || die "Do NOT run as root. Toolbox is a per-user install."
[[ -r /etc/os-release ]] || die "/etc/os-release not found."
# shellcheck disable=SC1091
. /etc/os-release
case "${ID:-}:${ID_LIKE:-}" in
*ubuntu*|*debian*) PKG_MGR=apt ;;
*fedora*|*rhel*) PKG_MGR=dnf ;;
*arch*) PKG_MGR=pacman ;;
*) die "Unsupported distro: ${PRETTY_NAME:-unknown}." ;;
esac
log "Detected: ${PRETTY_NAME:-unknown} (pkg: $PKG_MGR)"
confirm() {
(( ASSUME_YES )) && return 0
read -rp "$1 [y/N] " ans
[[ "$ans" =~ ^[Yy] ]]
}
ensure_pkg() {
local pkg="$1" probe="$2"
if eval "$probe" >/dev/null 2>&1; then return 0; fi
if ! confirm "Package '$pkg' is missing. Install via sudo $PKG_MGR?"; then
die "'$pkg' is required."
fi
case "$PKG_MGR" in
apt) run "sudo apt-get update -qq && sudo apt-get install -y '$pkg'" ;;
dnf) run "sudo dnf install -y '$pkg'" ;;
pacman) run "sudo pacman -Sy --noconfirm '$pkg'" ;;
esac
}
# Map deps: command-or-package-probe
ensure_pkg curl "command -v curl"
ensure_pkg jq "command -v jq"
ensure_pkg tar "command -v tar"
case "$PKG_MGR" in
apt) ensure_pkg libfuse2 "dpkg -s libfuse2" ;;
dnf) ensure_pkg fuse-libs "rpm -q fuse-libs" ;;
pacman) ensure_pkg fuse2 "pacman -Q fuse2" ;;
esac
ARCH_RAW="$(uname -m)"
case "$ARCH_RAW" in
x86_64) TOOLBOX_ARCH_KEY=linux ;;
aarch64) TOOLBOX_ARCH_KEY=linuxARM64 ;;
*) die "Unsupported architecture: $ARCH_RAW" ;;
esac
log "Querying JetBrains release feed..."
RELEASE_JSON="$(curl -fsSL 'https://data.services.jetbrains.com/products/releases?code=TBA&latest=true&type=release' \
-H 'Origin: https://www.jetbrains.com' \
-H 'Referer: https://www.jetbrains.com/toolbox/download/')"
TOOLBOX_URL="$(jq -r --arg k "$TOOLBOX_ARCH_KEY" '.TBA[0].downloads[$k].link // empty' <<<"$RELEASE_JSON")"
TOOLBOX_SHA="$(jq -r --arg k "$TOOLBOX_ARCH_KEY" '.TBA[0].downloads[$k].checksumLink // empty' <<<"$RELEASE_JSON")"
TOOLBOX_VER="$(jq -r '.TBA[0].version // "?"' <<<"$RELEASE_JSON")"
[[ -n "$TOOLBOX_URL" ]] || die "Could not resolve Toolbox download URL from release feed."
log "Latest Toolbox: $TOOLBOX_VER ($TOOLBOX_ARCH_KEY)"
INSTALL_DIR="$HOME/.local/share/JetBrains/Toolbox"
STAGE_DIR="$(mktemp -d -t toolbox.XXXXXX)"
trap 'rm -rf "$STAGE_DIR"' EXIT
TARBALL="$STAGE_DIR/toolbox.tar.gz"
log "Downloading..."
run "curl -fsSL -o '$TARBALL' '$TOOLBOX_URL'"
if [[ -n "$TOOLBOX_SHA" ]]; then
log "Verifying SHA-256..."
EXPECTED_SHA="$(curl -fsSL "$TOOLBOX_SHA" | awk '{print $1}')"
ACTUAL_SHA="$(sha256sum "$TARBALL" | awk '{print $1}')"
if [[ -n "$EXPECTED_SHA" && "$EXPECTED_SHA" != "$ACTUAL_SHA" ]]; then
die "SHA-256 mismatch! expected=$EXPECTED_SHA actual=$ACTUAL_SHA"
fi
log "SHA-256 ok."
else
warn "No checksum URL in release feed; skipping verification."
fi
log "Extracting to $INSTALL_DIR..."
run "mkdir -p '$INSTALL_DIR'"
run "tar -xzf '$TARBALL' --strip-components=1 -C '$INSTALL_DIR'"
BIN_PATH="$INSTALL_DIR/bin/jetbrains-toolbox"
DESKTOP_SRC="$INSTALL_DIR/bin/jetbrains-toolbox.desktop"
ICON_PATH="$INSTALL_DIR/bin/toolbox-tray-color.png"
[[ -x "$BIN_PATH" || $DRY_RUN -eq 1 ]] || die "Toolbox binary missing after extract."
# Optional ~/bin symlink
if [[ -d "$HOME/bin" ]]; then
run "ln -sfn '$BIN_PATH' '$HOME/bin/jetbrains-toolbox'"
fi
# .desktop launcher
APPS_DIR="$HOME/.local/share/applications"
run "mkdir -p '$APPS_DIR'"
DESKTOP_DST="$APPS_DIR/jetbrains-toolbox.desktop"
if [[ -f "$DESKTOP_SRC" ]] || (( DRY_RUN )); then
run "cp '$DESKTOP_SRC' '$DESKTOP_DST'"
run "sed -i 's|^Exec=.*|Exec=$BIN_PATH %u|' '$DESKTOP_DST'"
if [[ -f "$ICON_PATH" ]]; then
run "sed -i 's|^Icon=.*|Icon=$ICON_PATH|' '$DESKTOP_DST'"
fi
run "chmod 0755 '$DESKTOP_DST'"
log "Desktop entry installed: $DESKTOP_DST"
else
warn "No .desktop file in archive; skipping launcher."
fi
log "Done. JetBrains Toolbox $TOOLBOX_VER installed to $INSTALL_DIR"
| 1 | #!/usr/bin/env bash |
| 2 | # install-jetbrains-toolbox.sh — Install JetBrains Toolbox into the invoking user's home. |
| 3 | # Hardened: dependency check w/ auto-install, SHA-256 verification against JetBrains' |
| 4 | # release metadata, idempotent (replaces atomically), .desktop entry, --dry-run. |
| 5 | |
| 6 | set -euo pipefail |
| 7 | IFS=$'\n\t' |
| 8 | |
| 9 | readonly SCRIPT_NAME="${0##*/}" |
| 10 | DRY_RUN=0 |
| 11 | ASSUME_YES=0 |
| 12 | |
| 13 | usage() { |
| 14 | cat <<EOF |
| 15 | Usage: $SCRIPT_NAME [--dry-run] [--yes] [--help] |
| 16 | |
| 17 | Installs the latest JetBrains Toolbox to ~/.local/share/JetBrains/Toolbox and |
| 18 | creates a .desktop launcher in ~/.local/share/applications. The .tar.gz is |
| 19 | verified against the SHA-256 published in JetBrains' release feed. |
| 20 | |
| 21 | Do NOT run with sudo: Toolbox is a per-user install. If a missing system |
| 22 | package (curl/jq/tar/libfuse2) needs installing, the script will call sudo |
| 23 | just for that step. |
| 24 | |
| 25 | Options: |
| 26 | --yes, -y Auto-confirm prompts for installing missing system packages. |
| 27 | --dry-run Print actions without executing. |
| 28 | --help, -h Show this help. |
| 29 | EOF |
| 30 | } |
| 31 | |
| 32 | log() { printf '\033[1;34m[%s]\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*"; } |
| 33 | warn() { printf '\033[1;33m[%s] WARN:\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*" >&2; } |
| 34 | die() { printf '\033[1;31m[%s] ERROR:\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*" >&2; exit 1; } |
| 35 | run() { if (( DRY_RUN )); then printf ' DRY-RUN: %s\n' "$*"; else eval "$@"; fi; } |
| 36 | trap 'rc=$?; (( rc )) && printf "\033[1;31m[%s] failed at line %s (exit %d)\033[0m\n" "${SCRIPT_NAME%.sh}" "$LINENO" "$rc" >&2' ERR |
| 37 | |
| 38 | while (( $# )); do |
| 39 | case "$1" in |
| 40 | --dry-run) DRY_RUN=1 ;; |
| 41 | -y|--yes) ASSUME_YES=1 ;; |
| 42 | -h|--help) usage; exit 0 ;; |
| 43 | *) die "Unknown argument: $1 (try --help)" ;; |
| 44 | esac |
| 45 | shift |
| 46 | done |
| 47 | |
| 48 | (( EUID != 0 )) || die "Do NOT run as root. Toolbox is a per-user install." |
| 49 | |
| 50 | [[ -r /etc/os-release ]] || die "/etc/os-release not found." |
| 51 | # shellcheck disable=SC1091 |
| 52 | . /etc/os-release |
| 53 | case "${ID:-}:${ID_LIKE:-}" in |
| 54 | *ubuntu*|*debian*) PKG_MGR=apt ;; |
| 55 | *fedora*|*rhel*) PKG_MGR=dnf ;; |
| 56 | *arch*) PKG_MGR=pacman ;; |
| 57 | *) die "Unsupported distro: ${PRETTY_NAME:-unknown}." ;; |
| 58 | esac |
| 59 | log "Detected: ${PRETTY_NAME:-unknown} (pkg: $PKG_MGR)" |
| 60 | |
| 61 | confirm() { |
| 62 | (( ASSUME_YES )) && return 0 |
| 63 | read -rp "$1 [y/N] " ans |
| 64 | [[ "$ans" =~ ^[Yy] ]] |
| 65 | } |
| 66 | |
| 67 | ensure_pkg() { |
| 68 | local pkg="$1" probe="$2" |
| 69 | if eval "$probe" >/dev/null 2>&1; then return 0; fi |
| 70 | if ! confirm "Package '$pkg' is missing. Install via sudo $PKG_MGR?"; then |
| 71 | die "'$pkg' is required." |
| 72 | fi |
| 73 | case "$PKG_MGR" in |
| 74 | apt) run "sudo apt-get update -qq && sudo apt-get install -y '$pkg'" ;; |
| 75 | dnf) run "sudo dnf install -y '$pkg'" ;; |
| 76 | pacman) run "sudo pacman -Sy --noconfirm '$pkg'" ;; |
| 77 | esac |
| 78 | } |
| 79 | |
| 80 | # Map deps: command-or-package-probe |
| 81 | ensure_pkg curl "command -v curl" |
| 82 | ensure_pkg jq "command -v jq" |
| 83 | ensure_pkg tar "command -v tar" |
| 84 | case "$PKG_MGR" in |
| 85 | apt) ensure_pkg libfuse2 "dpkg -s libfuse2" ;; |
| 86 | dnf) ensure_pkg fuse-libs "rpm -q fuse-libs" ;; |
| 87 | pacman) ensure_pkg fuse2 "pacman -Q fuse2" ;; |
| 88 | esac |
| 89 | |
| 90 | ARCH_RAW="$(uname -m)" |
| 91 | case "$ARCH_RAW" in |
| 92 | x86_64) TOOLBOX_ARCH_KEY=linux ;; |
| 93 | aarch64) TOOLBOX_ARCH_KEY=linuxARM64 ;; |
| 94 | *) die "Unsupported architecture: $ARCH_RAW" ;; |
| 95 | esac |
| 96 | |
| 97 | log "Querying JetBrains release feed..." |
| 98 | RELEASE_JSON="$(curl -fsSL 'https://data.services.jetbrains.com/products/releases?code=TBA&latest=true&type=release' \ |
| 99 | -H 'Origin: https://www.jetbrains.com' \ |
| 100 | -H 'Referer: https://www.jetbrains.com/toolbox/download/')" |
| 101 | |
| 102 | TOOLBOX_URL="$(jq -r --arg k "$TOOLBOX_ARCH_KEY" '.TBA[0].downloads[$k].link // empty' <<<"$RELEASE_JSON")" |
| 103 | TOOLBOX_SHA="$(jq -r --arg k "$TOOLBOX_ARCH_KEY" '.TBA[0].downloads[$k].checksumLink // empty' <<<"$RELEASE_JSON")" |
| 104 | TOOLBOX_VER="$(jq -r '.TBA[0].version // "?"' <<<"$RELEASE_JSON")" |
| 105 | [[ -n "$TOOLBOX_URL" ]] || die "Could not resolve Toolbox download URL from release feed." |
| 106 | log "Latest Toolbox: $TOOLBOX_VER ($TOOLBOX_ARCH_KEY)" |
| 107 | |
| 108 | INSTALL_DIR="$HOME/.local/share/JetBrains/Toolbox" |
| 109 | STAGE_DIR="$(mktemp -d -t toolbox.XXXXXX)" |
| 110 | trap 'rm -rf "$STAGE_DIR"' EXIT |
| 111 | |
| 112 | TARBALL="$STAGE_DIR/toolbox.tar.gz" |
| 113 | log "Downloading..." |
| 114 | run "curl -fsSL -o '$TARBALL' '$TOOLBOX_URL'" |
| 115 | |
| 116 | if [[ -n "$TOOLBOX_SHA" ]]; then |
| 117 | log "Verifying SHA-256..." |
| 118 | EXPECTED_SHA="$(curl -fsSL "$TOOLBOX_SHA" | awk '{print $1}')" |
| 119 | ACTUAL_SHA="$(sha256sum "$TARBALL" | awk '{print $1}')" |
| 120 | if [[ -n "$EXPECTED_SHA" && "$EXPECTED_SHA" != "$ACTUAL_SHA" ]]; then |
| 121 | die "SHA-256 mismatch! expected=$EXPECTED_SHA actual=$ACTUAL_SHA" |
| 122 | fi |
| 123 | log "SHA-256 ok." |
| 124 | else |
| 125 | warn "No checksum URL in release feed; skipping verification." |
| 126 | fi |
| 127 | |
| 128 | log "Extracting to $INSTALL_DIR..." |
| 129 | run "mkdir -p '$INSTALL_DIR'" |
| 130 | run "tar -xzf '$TARBALL' --strip-components=1 -C '$INSTALL_DIR'" |
| 131 | |
| 132 | BIN_PATH="$INSTALL_DIR/bin/jetbrains-toolbox" |
| 133 | DESKTOP_SRC="$INSTALL_DIR/bin/jetbrains-toolbox.desktop" |
| 134 | ICON_PATH="$INSTALL_DIR/bin/toolbox-tray-color.png" |
| 135 | |
| 136 | [[ -x "$BIN_PATH" || $DRY_RUN -eq 1 ]] || die "Toolbox binary missing after extract." |
| 137 | |
| 138 | # Optional ~/bin symlink |
| 139 | if [[ -d "$HOME/bin" ]]; then |
| 140 | run "ln -sfn '$BIN_PATH' '$HOME/bin/jetbrains-toolbox'" |
| 141 | fi |
| 142 | |
| 143 | # .desktop launcher |
| 144 | APPS_DIR="$HOME/.local/share/applications" |
| 145 | run "mkdir -p '$APPS_DIR'" |
| 146 | DESKTOP_DST="$APPS_DIR/jetbrains-toolbox.desktop" |
| 147 | if [[ -f "$DESKTOP_SRC" ]] || (( DRY_RUN )); then |
| 148 | run "cp '$DESKTOP_SRC' '$DESKTOP_DST'" |
| 149 | run "sed -i 's|^Exec=.*|Exec=$BIN_PATH %u|' '$DESKTOP_DST'" |
| 150 | if [[ -f "$ICON_PATH" ]]; then |
| 151 | run "sed -i 's|^Icon=.*|Icon=$ICON_PATH|' '$DESKTOP_DST'" |
| 152 | fi |
| 153 | run "chmod 0755 '$DESKTOP_DST'" |
| 154 | log "Desktop entry installed: $DESKTOP_DST" |
| 155 | else |
| 156 | warn "No .desktop file in archive; skipping launcher." |
| 157 | fi |
| 158 | |
| 159 | log "Done. JetBrains Toolbox $TOOLBOX_VER installed to $INSTALL_DIR" |
| 160 |
install-libreoffice.sh
· 4.2 KiB · Bash
Surowy
#!/usr/bin/env bash
# install-libreoffice.sh — Install the latest stable LibreOffice from documentfoundation.org.
# Hardened: arch detection, version JSON when available with HTML fallback, MD5 verification,
# uninstalls bundled distro libreoffice* first to avoid conflicts, --dry-run.
set -euo pipefail
IFS=$'\n\t'
readonly SCRIPT_NAME="${0##*/}"
DRY_RUN=0
SKIP_REMOVE_DISTRO=0
usage() {
cat <<EOF
Usage: sudo $SCRIPT_NAME [--dry-run] [--keep-distro-libreoffice] [--help]
Detects the latest stable LibreOffice version on download.documentfoundation.org,
downloads the matching DEB tarball for your architecture, verifies the MD5 sum
published alongside it, and installs the .deb packages.
Options:
--keep-distro-libreoffice Don't purge any pre-installed distro libreoffice*
(default: purge to avoid library conflicts).
--dry-run Print actions without executing.
--help, -h Show this help.
EOF
}
log() { printf '\033[1;34m[%s]\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*"; }
warn() { printf '\033[1;33m[%s] WARN:\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*" >&2; }
die() { printf '\033[1;31m[%s] ERROR:\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*" >&2; exit 1; }
run() { if (( DRY_RUN )); then printf ' DRY-RUN: %s\n' "$*"; else eval "$@"; fi; }
trap 'rc=$?; (( rc )) && printf "\033[1;31m[%s] failed at line %s (exit %d)\033[0m\n" "${SCRIPT_NAME%.sh}" "$LINENO" "$rc" >&2' ERR
while (( $# )); do
case "$1" in
--dry-run) DRY_RUN=1 ;;
--keep-distro-libreoffice) SKIP_REMOVE_DISTRO=1 ;;
-h|--help) usage; exit 0 ;;
*) die "Unknown argument: $1 (try --help)" ;;
esac
shift
done
(( EUID == 0 )) || die "Must run as root. Try: sudo $SCRIPT_NAME"
[[ -r /etc/os-release ]] || die "/etc/os-release not found."
# shellcheck disable=SC1091
. /etc/os-release
case "${ID:-}:${ID_LIKE:-}" in
*ubuntu*|*debian*) : ;;
*) die "Unsupported distro: ${PRETTY_NAME:-unknown} (this script installs .deb packages)." ;;
esac
ARCH_RAW="$(uname -m)"
case "$ARCH_RAW" in
x86_64) LO_ARCH=x86_64; LO_SUFFIX=Linux_x86-64 ;;
aarch64) LO_ARCH=aarch64; LO_SUFFIX=Linux_aarch64 ;;
*) die "Unsupported architecture: $ARCH_RAW (LibreOffice ships x86_64 and aarch64)." ;;
esac
log "Detected: ${PRETTY_NAME:-unknown}, arch: $ARCH_RAW"
export DEBIAN_FRONTEND=noninteractive
log "Installing prerequisites..."
run "apt-get update -qq"
run "apt-get install -y curl wget tar coreutils ca-certificates"
log "Resolving latest stable LibreOffice version..."
INDEX="$(curl -fsSL https://download.documentfoundation.org/libreoffice/stable/)"
LATEST_VERSION="$(printf '%s\n' "$INDEX" \
| grep -oP '(?<=href=")[0-9]+\.[0-9]+\.[0-9]+(?=/")' \
| sort -V | tail -1 || true)"
[[ -n "$LATEST_VERSION" ]] || die "Could not detect latest version from index page."
log "Latest stable: $LATEST_VERSION"
BASE="https://download.documentfoundation.org/libreoffice/stable/${LATEST_VERSION}/deb/${LO_ARCH}"
ARCHIVE_NAME="LibreOffice_${LATEST_VERSION}_${LO_SUFFIX}_deb.tar.gz"
URL="${BASE}/${ARCHIVE_NAME}"
SUM_URL="${URL}.md5"
STAGE="$(mktemp -d -t lo.XXXXXX)"
trap 'rm -rf "$STAGE"' EXIT
ARCHIVE="$STAGE/$ARCHIVE_NAME"
log "Downloading $ARCHIVE_NAME..."
run "wget -q --show-progress -O '$ARCHIVE' '$URL'"
log "Verifying MD5..."
if EXPECTED_MD5="$(curl -fsSL "$SUM_URL" 2>/dev/null | awk '{print $1}')" && [[ -n "$EXPECTED_MD5" ]]; then
ACTUAL_MD5="$(md5sum "$ARCHIVE" | awk '{print $1}')"
[[ "$EXPECTED_MD5" == "$ACTUAL_MD5" ]] || die "MD5 mismatch: expected=$EXPECTED_MD5 actual=$ACTUAL_MD5"
log "MD5 ok."
else
warn "No MD5 published for $SUM_URL; skipping checksum."
fi
log "Extracting..."
run "tar -xzf '$ARCHIVE' -C '$STAGE'"
DEBS_DIR="$(find "$STAGE" -maxdepth 3 -type d -name DEBS | head -n1 || true)"
[[ -n "$DEBS_DIR" || $DRY_RUN -eq 1 ]] || die "DEBS/ directory not found in archive."
if (( ! SKIP_REMOVE_DISTRO )); then
if dpkg -l 'libreoffice*' 2>/dev/null | awk '/^ii/ {print $2}' | grep -q .; then
log "Removing distro-supplied libreoffice* packages to avoid conflicts..."
run "apt-get remove --purge -y 'libreoffice*'"
fi
fi
log "Installing .deb packages..."
run "dpkg -i -R '$DEBS_DIR'"
log "Resolving any missing dependencies..."
run "apt-get install -f -y"
log "Done. LibreOffice $LATEST_VERSION installed."
| 1 | #!/usr/bin/env bash |
| 2 | # install-libreoffice.sh — Install the latest stable LibreOffice from documentfoundation.org. |
| 3 | # Hardened: arch detection, version JSON when available with HTML fallback, MD5 verification, |
| 4 | # uninstalls bundled distro libreoffice* first to avoid conflicts, --dry-run. |
| 5 | |
| 6 | set -euo pipefail |
| 7 | IFS=$'\n\t' |
| 8 | |
| 9 | readonly SCRIPT_NAME="${0##*/}" |
| 10 | DRY_RUN=0 |
| 11 | SKIP_REMOVE_DISTRO=0 |
| 12 | |
| 13 | usage() { |
| 14 | cat <<EOF |
| 15 | Usage: sudo $SCRIPT_NAME [--dry-run] [--keep-distro-libreoffice] [--help] |
| 16 | |
| 17 | Detects the latest stable LibreOffice version on download.documentfoundation.org, |
| 18 | downloads the matching DEB tarball for your architecture, verifies the MD5 sum |
| 19 | published alongside it, and installs the .deb packages. |
| 20 | |
| 21 | Options: |
| 22 | --keep-distro-libreoffice Don't purge any pre-installed distro libreoffice* |
| 23 | (default: purge to avoid library conflicts). |
| 24 | --dry-run Print actions without executing. |
| 25 | --help, -h Show this help. |
| 26 | EOF |
| 27 | } |
| 28 | |
| 29 | log() { printf '\033[1;34m[%s]\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*"; } |
| 30 | warn() { printf '\033[1;33m[%s] WARN:\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*" >&2; } |
| 31 | die() { printf '\033[1;31m[%s] ERROR:\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*" >&2; exit 1; } |
| 32 | run() { if (( DRY_RUN )); then printf ' DRY-RUN: %s\n' "$*"; else eval "$@"; fi; } |
| 33 | trap 'rc=$?; (( rc )) && printf "\033[1;31m[%s] failed at line %s (exit %d)\033[0m\n" "${SCRIPT_NAME%.sh}" "$LINENO" "$rc" >&2' ERR |
| 34 | |
| 35 | while (( $# )); do |
| 36 | case "$1" in |
| 37 | --dry-run) DRY_RUN=1 ;; |
| 38 | --keep-distro-libreoffice) SKIP_REMOVE_DISTRO=1 ;; |
| 39 | -h|--help) usage; exit 0 ;; |
| 40 | *) die "Unknown argument: $1 (try --help)" ;; |
| 41 | esac |
| 42 | shift |
| 43 | done |
| 44 | |
| 45 | (( EUID == 0 )) || die "Must run as root. Try: sudo $SCRIPT_NAME" |
| 46 | |
| 47 | [[ -r /etc/os-release ]] || die "/etc/os-release not found." |
| 48 | # shellcheck disable=SC1091 |
| 49 | . /etc/os-release |
| 50 | case "${ID:-}:${ID_LIKE:-}" in |
| 51 | *ubuntu*|*debian*) : ;; |
| 52 | *) die "Unsupported distro: ${PRETTY_NAME:-unknown} (this script installs .deb packages)." ;; |
| 53 | esac |
| 54 | |
| 55 | ARCH_RAW="$(uname -m)" |
| 56 | case "$ARCH_RAW" in |
| 57 | x86_64) LO_ARCH=x86_64; LO_SUFFIX=Linux_x86-64 ;; |
| 58 | aarch64) LO_ARCH=aarch64; LO_SUFFIX=Linux_aarch64 ;; |
| 59 | *) die "Unsupported architecture: $ARCH_RAW (LibreOffice ships x86_64 and aarch64)." ;; |
| 60 | esac |
| 61 | log "Detected: ${PRETTY_NAME:-unknown}, arch: $ARCH_RAW" |
| 62 | |
| 63 | export DEBIAN_FRONTEND=noninteractive |
| 64 | |
| 65 | log "Installing prerequisites..." |
| 66 | run "apt-get update -qq" |
| 67 | run "apt-get install -y curl wget tar coreutils ca-certificates" |
| 68 | |
| 69 | log "Resolving latest stable LibreOffice version..." |
| 70 | INDEX="$(curl -fsSL https://download.documentfoundation.org/libreoffice/stable/)" |
| 71 | LATEST_VERSION="$(printf '%s\n' "$INDEX" \ |
| 72 | | grep -oP '(?<=href=")[0-9]+\.[0-9]+\.[0-9]+(?=/")' \ |
| 73 | | sort -V | tail -1 || true)" |
| 74 | [[ -n "$LATEST_VERSION" ]] || die "Could not detect latest version from index page." |
| 75 | log "Latest stable: $LATEST_VERSION" |
| 76 | |
| 77 | BASE="https://download.documentfoundation.org/libreoffice/stable/${LATEST_VERSION}/deb/${LO_ARCH}" |
| 78 | ARCHIVE_NAME="LibreOffice_${LATEST_VERSION}_${LO_SUFFIX}_deb.tar.gz" |
| 79 | URL="${BASE}/${ARCHIVE_NAME}" |
| 80 | SUM_URL="${URL}.md5" |
| 81 | |
| 82 | STAGE="$(mktemp -d -t lo.XXXXXX)" |
| 83 | trap 'rm -rf "$STAGE"' EXIT |
| 84 | ARCHIVE="$STAGE/$ARCHIVE_NAME" |
| 85 | |
| 86 | log "Downloading $ARCHIVE_NAME..." |
| 87 | run "wget -q --show-progress -O '$ARCHIVE' '$URL'" |
| 88 | |
| 89 | log "Verifying MD5..." |
| 90 | if EXPECTED_MD5="$(curl -fsSL "$SUM_URL" 2>/dev/null | awk '{print $1}')" && [[ -n "$EXPECTED_MD5" ]]; then |
| 91 | ACTUAL_MD5="$(md5sum "$ARCHIVE" | awk '{print $1}')" |
| 92 | [[ "$EXPECTED_MD5" == "$ACTUAL_MD5" ]] || die "MD5 mismatch: expected=$EXPECTED_MD5 actual=$ACTUAL_MD5" |
| 93 | log "MD5 ok." |
| 94 | else |
| 95 | warn "No MD5 published for $SUM_URL; skipping checksum." |
| 96 | fi |
| 97 | |
| 98 | log "Extracting..." |
| 99 | run "tar -xzf '$ARCHIVE' -C '$STAGE'" |
| 100 | DEBS_DIR="$(find "$STAGE" -maxdepth 3 -type d -name DEBS | head -n1 || true)" |
| 101 | [[ -n "$DEBS_DIR" || $DRY_RUN -eq 1 ]] || die "DEBS/ directory not found in archive." |
| 102 | |
| 103 | if (( ! SKIP_REMOVE_DISTRO )); then |
| 104 | if dpkg -l 'libreoffice*' 2>/dev/null | awk '/^ii/ {print $2}' | grep -q .; then |
| 105 | log "Removing distro-supplied libreoffice* packages to avoid conflicts..." |
| 106 | run "apt-get remove --purge -y 'libreoffice*'" |
| 107 | fi |
| 108 | fi |
| 109 | |
| 110 | log "Installing .deb packages..." |
| 111 | run "dpkg -i -R '$DEBS_DIR'" |
| 112 | log "Resolving any missing dependencies..." |
| 113 | run "apt-get install -f -y" |
| 114 | |
| 115 | log "Done. LibreOffice $LATEST_VERSION installed." |
| 116 |
install-network_drive.sh
· 7.2 KiB · Bash
Surowy
#!/usr/bin/env bash
# install-network_drive.sh — Auto-mount Synology (SMB/CIFS) shares under /mnt/Synology.
# Hardened: safe input handling, no eval-on-username, hostname validation,
# fstab managed via begin/end markers (idempotent re-run), creds file 0600,
# GNOME dock pinning is best-effort.
set -euo pipefail
IFS=$'\n\t'
readonly SCRIPT_NAME="${0##*/}"
DRY_RUN=0
SERVER_ADDR=""
SMB_USER=""
SMB_PASS=""
MASTER_DIR="/mnt/Synology"
usage() {
cat <<EOF
Usage: sudo $SCRIPT_NAME [options]
Discovers SMB shares on a Synology NAS and adds them to /etc/fstab so they
auto-mount under $MASTER_DIR. Creates a desktop launcher and (best-effort)
pins it to the top of the GNOME Dock.
Options:
--server HOST_OR_IP Synology address (skips prompt).
--user USERNAME SMB username (skips prompt).
--password-stdin Read SMB password from stdin (skips prompt).
(Otherwise the script prompts on the controlling tty.)
--mount-root DIR Override parent directory (default: $MASTER_DIR).
--dry-run Print actions without executing.
--help, -h Show this help.
EOF
}
log() { printf '\033[1;34m[%s]\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*"; }
warn() { printf '\033[1;33m[%s] WARN:\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*" >&2; }
die() { printf '\033[1;31m[%s] ERROR:\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*" >&2; exit 1; }
run() { if (( DRY_RUN )); then printf ' DRY-RUN: %s\n' "$*"; else eval "$@"; fi; }
trap 'rc=$?; (( rc )) && printf "\033[1;31m[%s] failed at line %s (exit %d)\033[0m\n" "${SCRIPT_NAME%.sh}" "$LINENO" "$rc" >&2' ERR
PASS_FROM_STDIN=0
while (( $# )); do
case "$1" in
--server) SERVER_ADDR="${2:?}"; shift ;;
--user) SMB_USER="${2:?}"; shift ;;
--password-stdin) PASS_FROM_STDIN=1 ;;
--mount-root) MASTER_DIR="${2:?}"; shift ;;
--dry-run) DRY_RUN=1 ;;
-h|--help) usage; exit 0 ;;
*) die "Unknown argument: $1 (try --help)" ;;
esac
shift
done
(( EUID == 0 )) || die "Must run as root. Try: sudo $SCRIPT_NAME"
[[ -r /etc/os-release ]] || die "/etc/os-release not found."
# shellcheck disable=SC1091
. /etc/os-release
case "${ID:-}:${ID_LIKE:-}" in
*ubuntu*|*debian*) : ;;
*) die "Unsupported distro: ${PRETTY_NAME:-unknown}." ;;
esac
ACTUAL_USER="${SUDO_USER:-${USER:-}}"
[[ -n "$ACTUAL_USER" && "$ACTUAL_USER" != "root" ]] || die "Run via sudo as a regular user."
# Use getent so we don't depend on eval/~expansion or shell glob safety.
USER_ENTRY="$(getent passwd "$ACTUAL_USER")" || die "User '$ACTUAL_USER' not found in passwd."
USER_HOME="$(awk -F: '{print $6}' <<<"$USER_ENTRY")"
USER_ID="$(awk -F: '{print $3}' <<<"$USER_ENTRY")"
USER_GID="$(awk -F: '{print $4}' <<<"$USER_ENTRY")"
export DEBIAN_FRONTEND=noninteractive
log "Installing cifs-utils, smbclient..."
run "apt-get update -qq"
run "apt-get install -y cifs-utils smbclient"
# --- Collect creds ---
[[ -n "$SERVER_ADDR" ]] || read -rp "Synology NAS address or IP: " SERVER_ADDR </dev/tty
SERVER_ADDR="${SERVER_ADDR#smb://}"
SERVER_ADDR="${SERVER_ADDR#//}"
SERVER_ADDR="${SERVER_ADDR%/}"
[[ "$SERVER_ADDR" =~ ^[A-Za-z0-9._-]+$ ]] || die "Invalid host/IP: '$SERVER_ADDR'"
[[ -n "$SMB_USER" ]] || read -rp "Synology username: " SMB_USER </dev/tty
[[ "$SMB_USER" =~ ^[A-Za-z0-9._@-]+$ ]] || die "Invalid SMB username."
if (( PASS_FROM_STDIN )); then
IFS= read -r SMB_PASS
else
read -rsp "Synology password: " SMB_PASS </dev/tty
echo
fi
[[ -n "$SMB_PASS" ]] || die "Password is empty."
# --- Credentials file ---
CRED_FILE="$USER_HOME/.smbcredentials_synology"
log "Writing credentials to $CRED_FILE (mode 0600)..."
if (( DRY_RUN )); then
printf ' DRY-RUN: write %s\n' "$CRED_FILE"
else
umask 077
{
printf 'username=%s\n' "$SMB_USER"
printf 'password=%s\n' "$SMB_PASS"
} >"$CRED_FILE"
chown "$USER_ID:$USER_GID" "$CRED_FILE"
chmod 0600 "$CRED_FILE"
fi
# --- Query shares ---
log "Discovering shares on //$SERVER_ADDR..."
if (( DRY_RUN )); then
SHARE_LIST=$'home\nphoto\nvideo'
else
SHARE_LIST="$(smbclient -L "//$SERVER_ADDR" -U "$SMB_USER%$SMB_PASS" -g 2>/dev/null \
| awk -F'|' '$1=="Disk" {print $2}' || true)"
fi
[[ -n "$SHARE_LIST" ]] || die "No shares returned. Check host, credentials, or network."
# --- Parent mount dir ---
run "mkdir -p '$MASTER_DIR'"
run "chown '$USER_ID:$USER_GID' '$MASTER_DIR'"
# --- Backup fstab once ---
if [[ ! -f /etc/fstab.bak.synology ]]; then
run "cp /etc/fstab /etc/fstab.bak.synology"
log "Backed up fstab -> /etc/fstab.bak.synology"
fi
MARK_BEGIN="# >>> synology-master >>> (managed by ${SCRIPT_NAME})"
MARK_END="# <<< synology-master <<<"
# Remove any previous managed block (so re-run replaces, not appends).
if grep -qF "$MARK_BEGIN" /etc/fstab; then
if (( DRY_RUN )); then
printf ' DRY-RUN: strip previous managed block from /etc/fstab\n'
else
sed -i "\|$MARK_BEGIN|,\|$MARK_END|d" /etc/fstab
fi
fi
log "Writing managed block to /etc/fstab..."
FSTAB_BLOCK="$MARK_BEGIN"$'\n'
while IFS= read -r SHARE; do
[[ -z "$SHARE" || "$SHARE" == "IPC\$" || "$SHARE" == "print\$" ]] && continue
MOUNT_POINT="$MASTER_DIR/$SHARE"
run "mkdir -p '$MOUNT_POINT'"
run "chown '$USER_ID:$USER_GID' '$MOUNT_POINT'"
FSTAB_BLOCK+="//${SERVER_ADDR}/${SHARE} ${MOUNT_POINT} cifs credentials=${CRED_FILE},uid=${USER_ID},gid=${USER_GID},_netdev,nofail,x-systemd.automount,x-systemd.idle-timeout=60 0 0"$'\n'
done <<<"$SHARE_LIST"
FSTAB_BLOCK+="$MARK_END"$'\n'
if (( DRY_RUN )); then
printf ' DRY-RUN: append fstab block:\n%s\n' "$FSTAB_BLOCK"
else
printf '%s' "$FSTAB_BLOCK" >>/etc/fstab
fi
log "Reloading systemd and mounting..."
run "systemctl daemon-reload"
run "mount -a -t cifs"
# --- Desktop entry ---
APPS_DIR="$USER_HOME/.local/share/applications"
DESKTOP_FILENAME="synology-master.desktop"
DESKTOP_FILE="$APPS_DIR/$DESKTOP_FILENAME"
run "install -d -o '$USER_ID' -g '$USER_GID' -m 0755 '$APPS_DIR'"
if (( DRY_RUN )); then
printf ' DRY-RUN: write %s\n' "$DESKTOP_FILE"
else
cat >"$DESKTOP_FILE" <<EOF
[Desktop Entry]
Name=Synology NAS
Comment=Open Synology Master Directory
Exec=xdg-open ${MASTER_DIR}
Icon=folder-remote
Terminal=false
Type=Application
Categories=Network;FileTools;
EOF
chown "$USER_ID:$USER_GID" "$DESKTOP_FILE"
chmod 0755 "$DESKTOP_FILE"
fi
# --- Best-effort GNOME dock pin ---
if (( ! DRY_RUN )) && sudo -u "$ACTUAL_USER" DBUS_SESSION_BUS_ADDRESS="unix:path=/run/user/$USER_ID/bus" gsettings list-keys org.gnome.shell >/dev/null 2>&1; then
log "Pinning to top of GNOME dock..."
CURRENT_FAVS="$(sudo -u "$ACTUAL_USER" DBUS_SESSION_BUS_ADDRESS="unix:path=/run/user/$USER_ID/bus" gsettings get org.gnome.shell favorite-apps 2>/dev/null || echo '[]')"
if [[ "$CURRENT_FAVS" != *"$DESKTOP_FILENAME"* ]]; then
CLEAN_FAVS="${CURRENT_FAVS#@as }"
if [[ "$CLEAN_FAVS" == "[]" || -z "$CLEAN_FAVS" ]]; then
NEW_FAVS="['$DESKTOP_FILENAME']"
else
NEW_FAVS="['$DESKTOP_FILENAME', ${CLEAN_FAVS:1}"
fi
sudo -u "$ACTUAL_USER" DBUS_SESSION_BUS_ADDRESS="unix:path=/run/user/$USER_ID/bus" \
gsettings set org.gnome.shell favorite-apps "$NEW_FAVS" || warn "Dock pin failed (non-fatal)."
else
log "Already pinned."
fi
else
log "GNOME not detected (or no active dbus session); skipping dock pin."
fi
log "Done. Shares mounted under $MASTER_DIR."
| 1 | #!/usr/bin/env bash |
| 2 | # install-network_drive.sh — Auto-mount Synology (SMB/CIFS) shares under /mnt/Synology. |
| 3 | # Hardened: safe input handling, no eval-on-username, hostname validation, |
| 4 | # fstab managed via begin/end markers (idempotent re-run), creds file 0600, |
| 5 | # GNOME dock pinning is best-effort. |
| 6 | |
| 7 | set -euo pipefail |
| 8 | IFS=$'\n\t' |
| 9 | |
| 10 | readonly SCRIPT_NAME="${0##*/}" |
| 11 | DRY_RUN=0 |
| 12 | SERVER_ADDR="" |
| 13 | SMB_USER="" |
| 14 | SMB_PASS="" |
| 15 | MASTER_DIR="/mnt/Synology" |
| 16 | |
| 17 | usage() { |
| 18 | cat <<EOF |
| 19 | Usage: sudo $SCRIPT_NAME [options] |
| 20 | |
| 21 | Discovers SMB shares on a Synology NAS and adds them to /etc/fstab so they |
| 22 | auto-mount under $MASTER_DIR. Creates a desktop launcher and (best-effort) |
| 23 | pins it to the top of the GNOME Dock. |
| 24 | |
| 25 | Options: |
| 26 | --server HOST_OR_IP Synology address (skips prompt). |
| 27 | --user USERNAME SMB username (skips prompt). |
| 28 | --password-stdin Read SMB password from stdin (skips prompt). |
| 29 | (Otherwise the script prompts on the controlling tty.) |
| 30 | --mount-root DIR Override parent directory (default: $MASTER_DIR). |
| 31 | --dry-run Print actions without executing. |
| 32 | --help, -h Show this help. |
| 33 | EOF |
| 34 | } |
| 35 | |
| 36 | log() { printf '\033[1;34m[%s]\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*"; } |
| 37 | warn() { printf '\033[1;33m[%s] WARN:\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*" >&2; } |
| 38 | die() { printf '\033[1;31m[%s] ERROR:\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*" >&2; exit 1; } |
| 39 | run() { if (( DRY_RUN )); then printf ' DRY-RUN: %s\n' "$*"; else eval "$@"; fi; } |
| 40 | trap 'rc=$?; (( rc )) && printf "\033[1;31m[%s] failed at line %s (exit %d)\033[0m\n" "${SCRIPT_NAME%.sh}" "$LINENO" "$rc" >&2' ERR |
| 41 | |
| 42 | PASS_FROM_STDIN=0 |
| 43 | while (( $# )); do |
| 44 | case "$1" in |
| 45 | --server) SERVER_ADDR="${2:?}"; shift ;; |
| 46 | --user) SMB_USER="${2:?}"; shift ;; |
| 47 | --password-stdin) PASS_FROM_STDIN=1 ;; |
| 48 | --mount-root) MASTER_DIR="${2:?}"; shift ;; |
| 49 | --dry-run) DRY_RUN=1 ;; |
| 50 | -h|--help) usage; exit 0 ;; |
| 51 | *) die "Unknown argument: $1 (try --help)" ;; |
| 52 | esac |
| 53 | shift |
| 54 | done |
| 55 | |
| 56 | (( EUID == 0 )) || die "Must run as root. Try: sudo $SCRIPT_NAME" |
| 57 | |
| 58 | [[ -r /etc/os-release ]] || die "/etc/os-release not found." |
| 59 | # shellcheck disable=SC1091 |
| 60 | . /etc/os-release |
| 61 | case "${ID:-}:${ID_LIKE:-}" in |
| 62 | *ubuntu*|*debian*) : ;; |
| 63 | *) die "Unsupported distro: ${PRETTY_NAME:-unknown}." ;; |
| 64 | esac |
| 65 | |
| 66 | ACTUAL_USER="${SUDO_USER:-${USER:-}}" |
| 67 | [[ -n "$ACTUAL_USER" && "$ACTUAL_USER" != "root" ]] || die "Run via sudo as a regular user." |
| 68 | # Use getent so we don't depend on eval/~expansion or shell glob safety. |
| 69 | USER_ENTRY="$(getent passwd "$ACTUAL_USER")" || die "User '$ACTUAL_USER' not found in passwd." |
| 70 | USER_HOME="$(awk -F: '{print $6}' <<<"$USER_ENTRY")" |
| 71 | USER_ID="$(awk -F: '{print $3}' <<<"$USER_ENTRY")" |
| 72 | USER_GID="$(awk -F: '{print $4}' <<<"$USER_ENTRY")" |
| 73 | |
| 74 | export DEBIAN_FRONTEND=noninteractive |
| 75 | log "Installing cifs-utils, smbclient..." |
| 76 | run "apt-get update -qq" |
| 77 | run "apt-get install -y cifs-utils smbclient" |
| 78 | |
| 79 | # --- Collect creds --- |
| 80 | [[ -n "$SERVER_ADDR" ]] || read -rp "Synology NAS address or IP: " SERVER_ADDR </dev/tty |
| 81 | SERVER_ADDR="${SERVER_ADDR#smb://}" |
| 82 | SERVER_ADDR="${SERVER_ADDR#//}" |
| 83 | SERVER_ADDR="${SERVER_ADDR%/}" |
| 84 | [[ "$SERVER_ADDR" =~ ^[A-Za-z0-9._-]+$ ]] || die "Invalid host/IP: '$SERVER_ADDR'" |
| 85 | |
| 86 | [[ -n "$SMB_USER" ]] || read -rp "Synology username: " SMB_USER </dev/tty |
| 87 | [[ "$SMB_USER" =~ ^[A-Za-z0-9._@-]+$ ]] || die "Invalid SMB username." |
| 88 | |
| 89 | if (( PASS_FROM_STDIN )); then |
| 90 | IFS= read -r SMB_PASS |
| 91 | else |
| 92 | read -rsp "Synology password: " SMB_PASS </dev/tty |
| 93 | echo |
| 94 | fi |
| 95 | [[ -n "$SMB_PASS" ]] || die "Password is empty." |
| 96 | |
| 97 | # --- Credentials file --- |
| 98 | CRED_FILE="$USER_HOME/.smbcredentials_synology" |
| 99 | log "Writing credentials to $CRED_FILE (mode 0600)..." |
| 100 | if (( DRY_RUN )); then |
| 101 | printf ' DRY-RUN: write %s\n' "$CRED_FILE" |
| 102 | else |
| 103 | umask 077 |
| 104 | { |
| 105 | printf 'username=%s\n' "$SMB_USER" |
| 106 | printf 'password=%s\n' "$SMB_PASS" |
| 107 | } >"$CRED_FILE" |
| 108 | chown "$USER_ID:$USER_GID" "$CRED_FILE" |
| 109 | chmod 0600 "$CRED_FILE" |
| 110 | fi |
| 111 | |
| 112 | # --- Query shares --- |
| 113 | log "Discovering shares on //$SERVER_ADDR..." |
| 114 | if (( DRY_RUN )); then |
| 115 | SHARE_LIST=$'home\nphoto\nvideo' |
| 116 | else |
| 117 | SHARE_LIST="$(smbclient -L "//$SERVER_ADDR" -U "$SMB_USER%$SMB_PASS" -g 2>/dev/null \ |
| 118 | | awk -F'|' '$1=="Disk" {print $2}' || true)" |
| 119 | fi |
| 120 | [[ -n "$SHARE_LIST" ]] || die "No shares returned. Check host, credentials, or network." |
| 121 | |
| 122 | # --- Parent mount dir --- |
| 123 | run "mkdir -p '$MASTER_DIR'" |
| 124 | run "chown '$USER_ID:$USER_GID' '$MASTER_DIR'" |
| 125 | |
| 126 | # --- Backup fstab once --- |
| 127 | if [[ ! -f /etc/fstab.bak.synology ]]; then |
| 128 | run "cp /etc/fstab /etc/fstab.bak.synology" |
| 129 | log "Backed up fstab -> /etc/fstab.bak.synology" |
| 130 | fi |
| 131 | |
| 132 | MARK_BEGIN="# >>> synology-master >>> (managed by ${SCRIPT_NAME})" |
| 133 | MARK_END="# <<< synology-master <<<" |
| 134 | # Remove any previous managed block (so re-run replaces, not appends). |
| 135 | if grep -qF "$MARK_BEGIN" /etc/fstab; then |
| 136 | if (( DRY_RUN )); then |
| 137 | printf ' DRY-RUN: strip previous managed block from /etc/fstab\n' |
| 138 | else |
| 139 | sed -i "\|$MARK_BEGIN|,\|$MARK_END|d" /etc/fstab |
| 140 | fi |
| 141 | fi |
| 142 | |
| 143 | log "Writing managed block to /etc/fstab..." |
| 144 | FSTAB_BLOCK="$MARK_BEGIN"$'\n' |
| 145 | while IFS= read -r SHARE; do |
| 146 | [[ -z "$SHARE" || "$SHARE" == "IPC\$" || "$SHARE" == "print\$" ]] && continue |
| 147 | MOUNT_POINT="$MASTER_DIR/$SHARE" |
| 148 | run "mkdir -p '$MOUNT_POINT'" |
| 149 | run "chown '$USER_ID:$USER_GID' '$MOUNT_POINT'" |
| 150 | FSTAB_BLOCK+="//${SERVER_ADDR}/${SHARE} ${MOUNT_POINT} cifs credentials=${CRED_FILE},uid=${USER_ID},gid=${USER_GID},_netdev,nofail,x-systemd.automount,x-systemd.idle-timeout=60 0 0"$'\n' |
| 151 | done <<<"$SHARE_LIST" |
| 152 | FSTAB_BLOCK+="$MARK_END"$'\n' |
| 153 | |
| 154 | if (( DRY_RUN )); then |
| 155 | printf ' DRY-RUN: append fstab block:\n%s\n' "$FSTAB_BLOCK" |
| 156 | else |
| 157 | printf '%s' "$FSTAB_BLOCK" >>/etc/fstab |
| 158 | fi |
| 159 | |
| 160 | log "Reloading systemd and mounting..." |
| 161 | run "systemctl daemon-reload" |
| 162 | run "mount -a -t cifs" |
| 163 | |
| 164 | # --- Desktop entry --- |
| 165 | APPS_DIR="$USER_HOME/.local/share/applications" |
| 166 | DESKTOP_FILENAME="synology-master.desktop" |
| 167 | DESKTOP_FILE="$APPS_DIR/$DESKTOP_FILENAME" |
| 168 | run "install -d -o '$USER_ID' -g '$USER_GID' -m 0755 '$APPS_DIR'" |
| 169 | |
| 170 | if (( DRY_RUN )); then |
| 171 | printf ' DRY-RUN: write %s\n' "$DESKTOP_FILE" |
| 172 | else |
| 173 | cat >"$DESKTOP_FILE" <<EOF |
| 174 | [Desktop Entry] |
| 175 | Name=Synology NAS |
| 176 | Comment=Open Synology Master Directory |
| 177 | Exec=xdg-open ${MASTER_DIR} |
| 178 | Icon=folder-remote |
| 179 | Terminal=false |
| 180 | Type=Application |
| 181 | Categories=Network;FileTools; |
| 182 | EOF |
| 183 | chown "$USER_ID:$USER_GID" "$DESKTOP_FILE" |
| 184 | chmod 0755 "$DESKTOP_FILE" |
| 185 | fi |
| 186 | |
| 187 | # --- Best-effort GNOME dock pin --- |
| 188 | if (( ! DRY_RUN )) && sudo -u "$ACTUAL_USER" DBUS_SESSION_BUS_ADDRESS="unix:path=/run/user/$USER_ID/bus" gsettings list-keys org.gnome.shell >/dev/null 2>&1; then |
| 189 | log "Pinning to top of GNOME dock..." |
| 190 | CURRENT_FAVS="$(sudo -u "$ACTUAL_USER" DBUS_SESSION_BUS_ADDRESS="unix:path=/run/user/$USER_ID/bus" gsettings get org.gnome.shell favorite-apps 2>/dev/null || echo '[]')" |
| 191 | if [[ "$CURRENT_FAVS" != *"$DESKTOP_FILENAME"* ]]; then |
| 192 | CLEAN_FAVS="${CURRENT_FAVS#@as }" |
| 193 | if [[ "$CLEAN_FAVS" == "[]" || -z "$CLEAN_FAVS" ]]; then |
| 194 | NEW_FAVS="['$DESKTOP_FILENAME']" |
| 195 | else |
| 196 | NEW_FAVS="['$DESKTOP_FILENAME', ${CLEAN_FAVS:1}" |
| 197 | fi |
| 198 | sudo -u "$ACTUAL_USER" DBUS_SESSION_BUS_ADDRESS="unix:path=/run/user/$USER_ID/bus" \ |
| 199 | gsettings set org.gnome.shell favorite-apps "$NEW_FAVS" || warn "Dock pin failed (non-fatal)." |
| 200 | else |
| 201 | log "Already pinned." |
| 202 | fi |
| 203 | else |
| 204 | log "GNOME not detected (or no active dbus session); skipping dock pin." |
| 205 | fi |
| 206 | |
| 207 | log "Done. Shares mounted under $MASTER_DIR." |
| 208 |
install-thunderbird.sh
· 3.0 KiB · Bash
Surowy
#!/usr/bin/env bash
# install-thunderbird.sh — Install Thunderbird.
# On Ubuntu: uses the Mozilla Team PPA (with APT pinning so Snap transition is bypassed).
# On Debian: uses the regular Debian package (no PPA available).
# Hardened: distro-detect, idempotent, --dry-run.
set -euo pipefail
IFS=$'\n\t'
readonly SCRIPT_NAME="${0##*/}"
DRY_RUN=0
usage() {
cat <<EOF
Usage: sudo $SCRIPT_NAME [--dry-run] [--help]
Installs Thunderbird.
- Ubuntu: removes Snap version (if any), adds Mozilla Team PPA, pins it,
and installs the .deb. This avoids the Ubuntu snap transition wrapper.
- Debian: installs from the standard Debian repos.
Options:
--dry-run Print actions without executing.
--help, -h Show this help.
EOF
}
log() { printf '\033[1;34m[%s]\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*"; }
warn() { printf '\033[1;33m[%s] WARN:\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*" >&2; }
die() { printf '\033[1;31m[%s] ERROR:\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*" >&2; exit 1; }
run() { if (( DRY_RUN )); then printf ' DRY-RUN: %s\n' "$*"; else eval "$@"; fi; }
trap 'rc=$?; (( rc )) && printf "\033[1;31m[%s] failed at line %s (exit %d)\033[0m\n" "${SCRIPT_NAME%.sh}" "$LINENO" "$rc" >&2' ERR
while (( $# )); do
case "$1" in
--dry-run) DRY_RUN=1 ;;
-h|--help) usage; exit 0 ;;
*) die "Unknown argument: $1 (try --help)" ;;
esac
shift
done
(( EUID == 0 )) || die "Must run as root. Try: sudo $SCRIPT_NAME"
[[ -r /etc/os-release ]] || die "/etc/os-release not found."
# shellcheck disable=SC1091
. /etc/os-release
log "Detected: ${PRETTY_NAME:-unknown} (id=${ID:-?})"
export DEBIAN_FRONTEND=noninteractive
case "${ID:-}" in
ubuntu)
log "Removing Thunderbird Snap (if present)..."
if command -v snap >/dev/null 2>&1; then
run "snap remove --purge thunderbird >/dev/null 2>&1 || true"
fi
log "Installing prerequisites..."
run "apt-get update -qq"
run "apt-get install -y software-properties-common"
log "Adding Mozilla Team PPA..."
PPA_LIST=/etc/apt/sources.list.d/mozillateam-ubuntu-ppa-*.list
# shellcheck disable=SC2086
if ! ls $PPA_LIST >/dev/null 2>&1; then
run "add-apt-repository -y ppa:mozillateam/ppa"
else
log "Mozilla Team PPA already configured."
fi
PIN=/etc/apt/preferences.d/mozillateamppa
log "Pinning Mozilla Team PPA so .deb wins over Snap transition wrapper..."
if [[ ! -f "$PIN" ]] || ! grep -q 'release o=LP-PPA-mozillateam' "$PIN"; then
if (( DRY_RUN )); then
printf ' DRY-RUN: write %s\n' "$PIN"
else
cat >"$PIN" <<'EOF'
Package: thunderbird*
Pin: release o=LP-PPA-mozillateam
Pin-Priority: 1001
EOF
fi
fi
log "Installing thunderbird from PPA..."
run "apt-get update -qq"
run "apt-get install -y --allow-downgrades thunderbird"
;;
debian)
log "Installing thunderbird from Debian repos..."
run "apt-get update -qq"
run "apt-get install -y thunderbird"
;;
*)
die "Unsupported distro: ${PRETTY_NAME:-unknown}. Supported: ubuntu, debian."
;;
esac
log "Done."
| 1 | #!/usr/bin/env bash |
| 2 | # install-thunderbird.sh — Install Thunderbird. |
| 3 | # On Ubuntu: uses the Mozilla Team PPA (with APT pinning so Snap transition is bypassed). |
| 4 | # On Debian: uses the regular Debian package (no PPA available). |
| 5 | # Hardened: distro-detect, idempotent, --dry-run. |
| 6 | |
| 7 | set -euo pipefail |
| 8 | IFS=$'\n\t' |
| 9 | |
| 10 | readonly SCRIPT_NAME="${0##*/}" |
| 11 | DRY_RUN=0 |
| 12 | |
| 13 | usage() { |
| 14 | cat <<EOF |
| 15 | Usage: sudo $SCRIPT_NAME [--dry-run] [--help] |
| 16 | |
| 17 | Installs Thunderbird. |
| 18 | - Ubuntu: removes Snap version (if any), adds Mozilla Team PPA, pins it, |
| 19 | and installs the .deb. This avoids the Ubuntu snap transition wrapper. |
| 20 | - Debian: installs from the standard Debian repos. |
| 21 | |
| 22 | Options: |
| 23 | --dry-run Print actions without executing. |
| 24 | --help, -h Show this help. |
| 25 | EOF |
| 26 | } |
| 27 | |
| 28 | log() { printf '\033[1;34m[%s]\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*"; } |
| 29 | warn() { printf '\033[1;33m[%s] WARN:\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*" >&2; } |
| 30 | die() { printf '\033[1;31m[%s] ERROR:\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*" >&2; exit 1; } |
| 31 | run() { if (( DRY_RUN )); then printf ' DRY-RUN: %s\n' "$*"; else eval "$@"; fi; } |
| 32 | trap 'rc=$?; (( rc )) && printf "\033[1;31m[%s] failed at line %s (exit %d)\033[0m\n" "${SCRIPT_NAME%.sh}" "$LINENO" "$rc" >&2' ERR |
| 33 | |
| 34 | while (( $# )); do |
| 35 | case "$1" in |
| 36 | --dry-run) DRY_RUN=1 ;; |
| 37 | -h|--help) usage; exit 0 ;; |
| 38 | *) die "Unknown argument: $1 (try --help)" ;; |
| 39 | esac |
| 40 | shift |
| 41 | done |
| 42 | |
| 43 | (( EUID == 0 )) || die "Must run as root. Try: sudo $SCRIPT_NAME" |
| 44 | |
| 45 | [[ -r /etc/os-release ]] || die "/etc/os-release not found." |
| 46 | # shellcheck disable=SC1091 |
| 47 | . /etc/os-release |
| 48 | log "Detected: ${PRETTY_NAME:-unknown} (id=${ID:-?})" |
| 49 | |
| 50 | export DEBIAN_FRONTEND=noninteractive |
| 51 | |
| 52 | case "${ID:-}" in |
| 53 | ubuntu) |
| 54 | log "Removing Thunderbird Snap (if present)..." |
| 55 | if command -v snap >/dev/null 2>&1; then |
| 56 | run "snap remove --purge thunderbird >/dev/null 2>&1 || true" |
| 57 | fi |
| 58 | |
| 59 | log "Installing prerequisites..." |
| 60 | run "apt-get update -qq" |
| 61 | run "apt-get install -y software-properties-common" |
| 62 | |
| 63 | log "Adding Mozilla Team PPA..." |
| 64 | PPA_LIST=/etc/apt/sources.list.d/mozillateam-ubuntu-ppa-*.list |
| 65 | # shellcheck disable=SC2086 |
| 66 | if ! ls $PPA_LIST >/dev/null 2>&1; then |
| 67 | run "add-apt-repository -y ppa:mozillateam/ppa" |
| 68 | else |
| 69 | log "Mozilla Team PPA already configured." |
| 70 | fi |
| 71 | |
| 72 | PIN=/etc/apt/preferences.d/mozillateamppa |
| 73 | log "Pinning Mozilla Team PPA so .deb wins over Snap transition wrapper..." |
| 74 | if [[ ! -f "$PIN" ]] || ! grep -q 'release o=LP-PPA-mozillateam' "$PIN"; then |
| 75 | if (( DRY_RUN )); then |
| 76 | printf ' DRY-RUN: write %s\n' "$PIN" |
| 77 | else |
| 78 | cat >"$PIN" <<'EOF' |
| 79 | Package: thunderbird* |
| 80 | Pin: release o=LP-PPA-mozillateam |
| 81 | Pin-Priority: 1001 |
| 82 | EOF |
| 83 | fi |
| 84 | fi |
| 85 | |
| 86 | log "Installing thunderbird from PPA..." |
| 87 | run "apt-get update -qq" |
| 88 | run "apt-get install -y --allow-downgrades thunderbird" |
| 89 | ;; |
| 90 | |
| 91 | debian) |
| 92 | log "Installing thunderbird from Debian repos..." |
| 93 | run "apt-get update -qq" |
| 94 | run "apt-get install -y thunderbird" |
| 95 | ;; |
| 96 | |
| 97 | *) |
| 98 | die "Unsupported distro: ${PRETTY_NAME:-unknown}. Supported: ubuntu, debian." |
| 99 | ;; |
| 100 | esac |
| 101 | |
| 102 | log "Done." |
| 103 |
install-vscode.sh
· 2.8 KiB · Bash
Surowy
#!/usr/bin/env bash
# install-vscode.sh — Install Visual Studio Code from Microsoft's APT repository.
# Hardened: arch-aware, signed-by keyring, idempotent, --dry-run.
set -euo pipefail
IFS=$'\n\t'
readonly SCRIPT_NAME="${0##*/}"
DRY_RUN=0
INSIDERS=0
usage() {
cat <<EOF
Usage: sudo $SCRIPT_NAME [--dry-run] [--insiders] [--help]
Configures the official Microsoft 'vscode' APT repository (signed-by keyring)
and installs Visual Studio Code. Idempotent: safe to re-run.
Options:
--insiders Install the 'code-insiders' build instead of stable 'code'.
--dry-run Print actions without executing.
--help, -h Show this help.
EOF
}
log() { printf '\033[1;34m[%s]\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*"; }
warn() { printf '\033[1;33m[%s] WARN:\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*" >&2; }
die() { printf '\033[1;31m[%s] ERROR:\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*" >&2; exit 1; }
run() { if (( DRY_RUN )); then printf ' DRY-RUN: %s\n' "$*"; else eval "$@"; fi; }
trap 'rc=$?; (( rc )) && printf "\033[1;31m[%s] failed at line %s (exit %d)\033[0m\n" "${SCRIPT_NAME%.sh}" "$LINENO" "$rc" >&2' ERR
while (( $# )); do
case "$1" in
--dry-run) DRY_RUN=1 ;;
--insiders) INSIDERS=1 ;;
-h|--help) usage; exit 0 ;;
*) die "Unknown argument: $1 (try --help)" ;;
esac
shift
done
(( EUID == 0 )) || die "Must run as root. Try: sudo $SCRIPT_NAME"
[[ -r /etc/os-release ]] || die "/etc/os-release not found."
# shellcheck disable=SC1091
. /etc/os-release
case "${ID:-}:${ID_LIKE:-}" in
*ubuntu*|*debian*) : ;;
*) die "Unsupported distro: ${PRETTY_NAME:-unknown}." ;;
esac
ARCH="$(dpkg --print-architecture)"
case "$ARCH" in
amd64|arm64|armhf) : ;;
*) die "VS Code is published for amd64/arm64/armhf (detected: $ARCH)." ;;
esac
log "Detected: ${PRETTY_NAME:-unknown}, arch: $ARCH"
export DEBIAN_FRONTEND=noninteractive
KEYRING=/etc/apt/keyrings/packages.microsoft.gpg
SOURCES=/etc/apt/sources.list.d/vscode.list
PKG=$([[ $INSIDERS -eq 1 ]] && echo code-insiders || echo code)
log "Installing prerequisites..."
run "apt-get update -qq"
run "apt-get install -y wget gpg apt-transport-https ca-certificates"
log "Configuring Microsoft vscode APT repository..."
run "install -d -m 0755 /etc/apt/keyrings"
if [[ ! -s "$KEYRING" ]]; then
run "wget -qO- https://packages.microsoft.com/keys/microsoft.asc | gpg --dearmor -o '$KEYRING'"
run "chmod 0644 '$KEYRING'"
fi
DESIRED_SRC="deb [arch=amd64,arm64,armhf signed-by=${KEYRING}] https://packages.microsoft.com/repos/code stable main"
if [[ ! -f "$SOURCES" ]] || ! grep -qxF "$DESIRED_SRC" "$SOURCES"; then
run "printf '%s\n' '$DESIRED_SRC' > '$SOURCES'"
fi
log "Installing $PKG..."
run "apt-get update -qq"
run "apt-get install -y '$PKG'"
if (( ! DRY_RUN )) && command -v "$PKG" >/dev/null 2>&1; then
log "Installed: $("$PKG" --version 2>/dev/null | head -n1 || echo "$PKG")"
fi
log "Done."
| 1 | #!/usr/bin/env bash |
| 2 | # install-vscode.sh — Install Visual Studio Code from Microsoft's APT repository. |
| 3 | # Hardened: arch-aware, signed-by keyring, idempotent, --dry-run. |
| 4 | |
| 5 | set -euo pipefail |
| 6 | IFS=$'\n\t' |
| 7 | |
| 8 | readonly SCRIPT_NAME="${0##*/}" |
| 9 | DRY_RUN=0 |
| 10 | INSIDERS=0 |
| 11 | |
| 12 | usage() { |
| 13 | cat <<EOF |
| 14 | Usage: sudo $SCRIPT_NAME [--dry-run] [--insiders] [--help] |
| 15 | |
| 16 | Configures the official Microsoft 'vscode' APT repository (signed-by keyring) |
| 17 | and installs Visual Studio Code. Idempotent: safe to re-run. |
| 18 | |
| 19 | Options: |
| 20 | --insiders Install the 'code-insiders' build instead of stable 'code'. |
| 21 | --dry-run Print actions without executing. |
| 22 | --help, -h Show this help. |
| 23 | EOF |
| 24 | } |
| 25 | |
| 26 | log() { printf '\033[1;34m[%s]\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*"; } |
| 27 | warn() { printf '\033[1;33m[%s] WARN:\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*" >&2; } |
| 28 | die() { printf '\033[1;31m[%s] ERROR:\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*" >&2; exit 1; } |
| 29 | run() { if (( DRY_RUN )); then printf ' DRY-RUN: %s\n' "$*"; else eval "$@"; fi; } |
| 30 | trap 'rc=$?; (( rc )) && printf "\033[1;31m[%s] failed at line %s (exit %d)\033[0m\n" "${SCRIPT_NAME%.sh}" "$LINENO" "$rc" >&2' ERR |
| 31 | |
| 32 | while (( $# )); do |
| 33 | case "$1" in |
| 34 | --dry-run) DRY_RUN=1 ;; |
| 35 | --insiders) INSIDERS=1 ;; |
| 36 | -h|--help) usage; exit 0 ;; |
| 37 | *) die "Unknown argument: $1 (try --help)" ;; |
| 38 | esac |
| 39 | shift |
| 40 | done |
| 41 | |
| 42 | (( EUID == 0 )) || die "Must run as root. Try: sudo $SCRIPT_NAME" |
| 43 | |
| 44 | [[ -r /etc/os-release ]] || die "/etc/os-release not found." |
| 45 | # shellcheck disable=SC1091 |
| 46 | . /etc/os-release |
| 47 | case "${ID:-}:${ID_LIKE:-}" in |
| 48 | *ubuntu*|*debian*) : ;; |
| 49 | *) die "Unsupported distro: ${PRETTY_NAME:-unknown}." ;; |
| 50 | esac |
| 51 | |
| 52 | ARCH="$(dpkg --print-architecture)" |
| 53 | case "$ARCH" in |
| 54 | amd64|arm64|armhf) : ;; |
| 55 | *) die "VS Code is published for amd64/arm64/armhf (detected: $ARCH)." ;; |
| 56 | esac |
| 57 | log "Detected: ${PRETTY_NAME:-unknown}, arch: $ARCH" |
| 58 | |
| 59 | export DEBIAN_FRONTEND=noninteractive |
| 60 | |
| 61 | KEYRING=/etc/apt/keyrings/packages.microsoft.gpg |
| 62 | SOURCES=/etc/apt/sources.list.d/vscode.list |
| 63 | PKG=$([[ $INSIDERS -eq 1 ]] && echo code-insiders || echo code) |
| 64 | |
| 65 | log "Installing prerequisites..." |
| 66 | run "apt-get update -qq" |
| 67 | run "apt-get install -y wget gpg apt-transport-https ca-certificates" |
| 68 | |
| 69 | log "Configuring Microsoft vscode APT repository..." |
| 70 | run "install -d -m 0755 /etc/apt/keyrings" |
| 71 | if [[ ! -s "$KEYRING" ]]; then |
| 72 | run "wget -qO- https://packages.microsoft.com/keys/microsoft.asc | gpg --dearmor -o '$KEYRING'" |
| 73 | run "chmod 0644 '$KEYRING'" |
| 74 | fi |
| 75 | |
| 76 | DESIRED_SRC="deb [arch=amd64,arm64,armhf signed-by=${KEYRING}] https://packages.microsoft.com/repos/code stable main" |
| 77 | if [[ ! -f "$SOURCES" ]] || ! grep -qxF "$DESIRED_SRC" "$SOURCES"; then |
| 78 | run "printf '%s\n' '$DESIRED_SRC' > '$SOURCES'" |
| 79 | fi |
| 80 | |
| 81 | log "Installing $PKG..." |
| 82 | run "apt-get update -qq" |
| 83 | run "apt-get install -y '$PKG'" |
| 84 | |
| 85 | if (( ! DRY_RUN )) && command -v "$PKG" >/dev/null 2>&1; then |
| 86 | log "Installed: $("$PKG" --version 2>/dev/null | head -n1 || echo "$PKG")" |
| 87 | fi |
| 88 | log "Done." |
| 89 |
install_font.sh
· 1.7 KiB · Bash
Surowy
#!/bin/bash
# Exit immediately if a command exits with a non-zero status
set -e
FONT_NAME="UbuntuSans"
FONT_ZIP="${FONT_NAME}.zip"
# Simplified directory: removed the "NerdFonts" subfolder
FONT_DIR="$HOME/.local/share/fonts/${FONT_NAME}"
TMP_DIR=$(mktemp -d)
echo "Searching for the latest release of $FONT_NAME Nerd Font..."
# Use GitHub API to find the latest release download URL for UbuntuSans.zip
DOWNLOAD_URL=$(curl -s https://api.github.com/repos/ryanoasis/nerd-fonts/releases/latest \
| grep "browser_download_url.*${FONT_ZIP}" \
| cut -d '"' -f 4)
if [ -z "$DOWNLOAD_URL" ]; then
echo "Error: Could not find the download URL for $FONT_NAME. Check your internet connection or GitHub API limits."
rm -rf "$TMP_DIR"
exit 1
fi
echo "Latest version found!"
echo "Downloading from: $DOWNLOAD_URL"
# Download the zip file to the temporary directory
curl -L -q "$DOWNLOAD_URL" -o "$TMP_DIR/$FONT_ZIP"
echo "Extracting fonts..."
# Ensure unzip is installed (will fail gracefully if not)
if ! command -v unzip &> /dev/null; then
echo "Error: 'unzip' is not installed. Please install it using 'sudo apt install unzip' and try again."
rm -rf "$TMP_DIR"
exit 1
fi
unzip -q -o "$TMP_DIR/$FONT_ZIP" -d "$TMP_DIR/extracted"
echo "Installing fonts to $FONT_DIR..."
# Create the simplified font directory if it doesn't exist
mkdir -p "$FONT_DIR"
# Move only the TrueType (.ttf) or OpenType (.otf) files to the fonts directory
find "$TMP_DIR/extracted" -name '*.[ot]tf' -type f -exec cp {} "$FONT_DIR/" \;
echo "Updating the system font cache..."
fc-cache -f "$FONT_DIR"
echo "Cleaning up temporary files..."
rm -rf "$TMP_DIR"
echo "Done! $FONT_NAME Nerd Font has been successfully installed."
| 1 | #!/bin/bash |
| 2 | |
| 3 | # Exit immediately if a command exits with a non-zero status |
| 4 | set -e |
| 5 | |
| 6 | FONT_NAME="UbuntuSans" |
| 7 | FONT_ZIP="${FONT_NAME}.zip" |
| 8 | # Simplified directory: removed the "NerdFonts" subfolder |
| 9 | FONT_DIR="$HOME/.local/share/fonts/${FONT_NAME}" |
| 10 | TMP_DIR=$(mktemp -d) |
| 11 | |
| 12 | echo "Searching for the latest release of $FONT_NAME Nerd Font..." |
| 13 | |
| 14 | # Use GitHub API to find the latest release download URL for UbuntuSans.zip |
| 15 | DOWNLOAD_URL=$(curl -s https://api.github.com/repos/ryanoasis/nerd-fonts/releases/latest \ |
| 16 | | grep "browser_download_url.*${FONT_ZIP}" \ |
| 17 | | cut -d '"' -f 4) |
| 18 | |
| 19 | if [ -z "$DOWNLOAD_URL" ]; then |
| 20 | echo "Error: Could not find the download URL for $FONT_NAME. Check your internet connection or GitHub API limits." |
| 21 | rm -rf "$TMP_DIR" |
| 22 | exit 1 |
| 23 | fi |
| 24 | |
| 25 | echo "Latest version found!" |
| 26 | echo "Downloading from: $DOWNLOAD_URL" |
| 27 | |
| 28 | # Download the zip file to the temporary directory |
| 29 | curl -L -q "$DOWNLOAD_URL" -o "$TMP_DIR/$FONT_ZIP" |
| 30 | |
| 31 | echo "Extracting fonts..." |
| 32 | # Ensure unzip is installed (will fail gracefully if not) |
| 33 | if ! command -v unzip &> /dev/null; then |
| 34 | echo "Error: 'unzip' is not installed. Please install it using 'sudo apt install unzip' and try again." |
| 35 | rm -rf "$TMP_DIR" |
| 36 | exit 1 |
| 37 | fi |
| 38 | |
| 39 | unzip -q -o "$TMP_DIR/$FONT_ZIP" -d "$TMP_DIR/extracted" |
| 40 | |
| 41 | echo "Installing fonts to $FONT_DIR..." |
| 42 | # Create the simplified font directory if it doesn't exist |
| 43 | mkdir -p "$FONT_DIR" |
| 44 | |
| 45 | # Move only the TrueType (.ttf) or OpenType (.otf) files to the fonts directory |
| 46 | find "$TMP_DIR/extracted" -name '*.[ot]tf' -type f -exec cp {} "$FONT_DIR/" \; |
| 47 | |
| 48 | echo "Updating the system font cache..." |
| 49 | fc-cache -f "$FONT_DIR" |
| 50 | |
| 51 | echo "Cleaning up temporary files..." |
| 52 | rm -rf "$TMP_DIR" |
| 53 | |
| 54 | echo "Done! $FONT_NAME Nerd Font has been successfully installed." |