Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
069457b3e9 | ||
|
|
ea1a8a7a7e | ||
|
|
8221ac5325 | ||
|
|
9911f6d01d | ||
|
|
ba1e07744e | ||
|
|
b5be992d82 | ||
|
|
76bc2c7ade | ||
|
|
32dc96ef0f | ||
|
|
d28b602827 | ||
|
|
afe1a1b610 | ||
|
|
9e96aaa411 | ||
|
|
25c6b71e59 | ||
|
|
f78bfe5859 | ||
|
|
3fbe9e85e2 | ||
|
|
01ec151ade | ||
|
|
2b4732d0e3 | ||
|
|
f7ff06cce6 | ||
|
|
d2397ecea7 | ||
|
|
8e8f4890b7 | ||
|
|
229422eda0 | ||
|
|
9723a6ad93 | ||
|
|
df5dca96c6 | ||
|
|
b1071ab4b9 | ||
|
|
a9f55945b8 | ||
|
|
f931db0808 | ||
|
|
daa2ee755c | ||
|
|
6bfc2804ab | ||
|
|
2cc767ecc2 | ||
|
|
82b9e6bf2f | ||
|
|
99dc9a4ef5 | ||
|
|
c1ea14e145 | ||
|
|
03ca259c4c | ||
|
|
97773515eb | ||
|
|
fbe1f074da | ||
|
|
db7dcb6f03 | ||
|
|
e328199618 | ||
|
|
e6607f07af | ||
|
|
54da852ac3 | ||
|
|
d375a09623 | ||
|
|
b2b25843ff | ||
|
|
2702682fae | ||
|
|
970cd2ccf6 | ||
|
|
38038e0fa1 | ||
|
|
9ba36582e4 | ||
|
|
022d4f9e77 | ||
|
|
70fc5c2f78 | ||
|
|
a178439545 | ||
|
|
a93d367974 | ||
|
|
bcc86814b5 | ||
|
|
462efef034 | ||
|
|
f0c62a5e7f | ||
|
|
df4c63657b | ||
|
|
9c201f0e97 | ||
|
|
c4c52cfb48 | ||
|
|
7c14c67ad0 | ||
|
|
4882bee0ad | ||
|
|
bbfd877ee5 | ||
|
|
1288b47c34 | ||
|
|
d849b22001 | ||
|
|
9fcdb24988 | ||
|
|
3e0bedd9c8 | ||
|
|
481cc94fa8 | ||
|
|
28e1e26b0e | ||
|
|
bddcd170c3 | ||
|
|
0a85d265cb | ||
|
|
c13945ced5 | ||
|
|
a33aa5f6be | ||
|
|
d9a62cf2a8 | ||
|
|
7287d8ef87 | ||
|
|
7c761a4895 | ||
|
|
fef99326c3 | ||
|
|
ac7eba14b0 | ||
|
|
f6bf229c8d | ||
|
|
01e17888fe | ||
|
|
8ac0c1eaf2 | ||
|
|
600f170d00 | ||
|
|
cbe29a05f5 | ||
|
|
8399adbfa0 | ||
|
|
2c81a48fe9 | ||
|
|
d03ced93bf | ||
|
|
8b1fc5f01c | ||
|
|
02cd9a853e | ||
|
|
648777a13e | ||
|
|
5264470127 | ||
|
|
f923dcb758 | ||
|
|
c92ca8a51f | ||
|
|
19ca7fa905 | ||
|
|
6bacb03687 | ||
|
|
5f8800ab44 | ||
|
|
8bfb3272c0 | ||
|
|
1a48280b14 | ||
|
|
9a272689eb | ||
|
|
6e4d052170 | ||
|
|
ceb3386c57 | ||
|
|
d22b24e54c | ||
|
|
28e4c112af | ||
|
|
9ec52aa49f | ||
|
|
1b28e90c62 | ||
|
|
1e31712b60 | ||
|
|
5faae67d11 | ||
|
|
ee72ede116 | ||
|
|
f5244ac062 | ||
|
|
9a089112c3 | ||
|
|
e64a857a43 | ||
|
|
ddd7d4193a | ||
|
|
83a1c8ce48 | ||
|
|
b29fa3b30c | ||
|
|
cd0bcfd214 | ||
|
|
a91c41871a | ||
|
|
9698f0e506 | ||
|
|
9e22f007b9 | ||
|
|
d472fb61aa | ||
|
|
9108ee8266 | ||
|
|
a7f7452b2b | ||
|
|
bc67399ebc | ||
|
|
02b037d0fc | ||
|
|
e567957ea0 | ||
|
|
67bdd3e863 | ||
|
|
fa573bce8f | ||
|
|
241d53ebc4 | ||
|
|
1225230a07 | ||
|
|
9c43190202 | ||
|
|
9477638a28 | ||
|
|
f16ad711fb | ||
|
|
15ef317029 | ||
|
|
a6e4d7a256 | ||
|
|
6106ca7684 | ||
|
|
1088029ae6 | ||
|
|
eb4c89759b | ||
|
|
1dc5d72ac6 | ||
|
|
d49703c5d5 | ||
|
|
066f2e353e | ||
|
|
c011f03aee | ||
|
|
c4b0516c45 | ||
|
|
4da29872fc | ||
|
|
d7a0e2c5f5 | ||
|
|
d60a93814b | ||
|
|
d27935eedd | ||
|
|
e77232b3ca | ||
|
|
d6dce2d91e | ||
|
|
4e3cccff64 | ||
|
|
91f033743d | ||
|
|
9fcf21c55e | ||
|
|
5300386941 | ||
|
|
dfad345be3 | ||
|
|
1b16878ea8 | ||
|
|
30b8b8241a | ||
|
|
a4056b9e82 | ||
|
|
6696e0d05a | ||
|
|
6d15af029e | ||
|
|
bc0a592fa1 | ||
|
|
b3f909e287 | ||
|
|
89e20993da | ||
|
|
9a006883b8 | ||
|
|
85f02f4498 | ||
|
|
bb9bbfda16 | ||
|
|
9ea1cc3f72 | ||
|
|
b79103a0a6 | ||
|
|
e5bafe1721 | ||
|
|
322d03ed4c | ||
|
|
60a159c3ea | ||
|
|
84e6fdd429 | ||
|
|
8fe11776cb | ||
|
|
0737d0c647 | ||
|
|
d72fa1a712 | ||
|
|
08e9e6c799 | ||
|
|
ac66e896dd | ||
|
|
0712be626b | ||
|
|
3f8b81562b | ||
|
|
96d1dc695d | ||
|
|
c039ab6ea0 | ||
|
|
6b85556a53 | ||
|
|
cf9a85e61b | ||
|
|
75e047d70e | ||
|
|
742ec484a7 | ||
|
|
1b7262c0cd | ||
|
|
e387209c10 | ||
|
|
f5d59ec194 | ||
|
|
2ee1c935ac | ||
|
|
e41c1a4c51 | ||
|
|
60dfe19049 | ||
|
|
c32771a4ff | ||
|
|
080511d0bd | ||
|
|
d8bdfefdf1 | ||
|
|
5f5f9c0e71 | ||
|
|
30387a4f08 | ||
|
|
0c51363d86 | ||
|
|
043fbaef0b | ||
|
|
ed5587712e | ||
|
|
58cc76c317 | ||
|
|
e82ee06e1d | ||
|
|
bc8cb4a237 | ||
|
|
ae90a9f4c4 | ||
|
|
7e661ca2de | ||
|
|
9f22ed4304 | ||
|
|
1484b004be | ||
|
|
0a4206b890 | ||
|
|
02a1e25df2 | ||
|
|
7ca0a6fb88 | ||
|
|
25df408e37 | ||
|
|
3eab1f98d5 | ||
|
|
6c895b509a | ||
|
|
2ece711e1a | ||
|
|
39a7e7b40f | ||
|
|
6d5d872b71 | ||
|
|
128cfe8c87 | ||
|
|
e1c2705fdd | ||
|
|
368bc11acf | ||
|
|
a068d57ba5 | ||
|
|
ffee8c2e47 | ||
|
|
9ff5792790 | ||
|
|
6a2d9b0fee | ||
|
|
9b2c764181 | ||
|
|
3d5a5e7718 | ||
|
|
47e89b3b09 | ||
|
|
f0f80e2924 | ||
|
|
b08e457146 | ||
|
|
56e34bc346 | ||
|
|
e9e9993dfc | ||
|
|
ff4c6702b7 | ||
|
|
87dea45295 | ||
|
|
0abf481cf6 | ||
|
|
a75299f7b4 | ||
|
|
eeb87c5bfc | ||
|
|
4879b418db | ||
|
|
f944271488 | ||
|
|
4be2e5ea87 | ||
|
|
9d528a6491 | ||
|
|
bef205ae84 | ||
|
|
9e49e3e4d7 | ||
|
|
94e7e79c76 | ||
|
|
55e88bd018 | ||
|
|
18f1bc1543 | ||
|
|
0d7c7e9ab7 | ||
|
|
64fecf16fb | ||
|
|
c2ca5f659c | ||
|
|
3248327e56 | ||
|
|
fd984c4a16 | ||
|
|
c2a0ef0bd2 | ||
|
|
dc64123fd9 | ||
|
|
affe6e1487 | ||
|
|
0237f8bf70 | ||
|
|
d4db72260a | ||
|
|
e980198c08 | ||
|
|
21c12865f5 | ||
|
|
5b9d7a983e | ||
|
|
d7e6d81126 | ||
|
|
4a975db1c0 |
+15
@@ -0,0 +1,15 @@
|
||||
# User-specific configuration — never commit personal settings.
|
||||
# Copy doc/profile.conf.example to profile.conf and customise it.
|
||||
profile.conf
|
||||
|
||||
# Compiled / generated artefacts
|
||||
*.pyc
|
||||
__pycache__/
|
||||
|
||||
# macOS noise
|
||||
.DS_Store
|
||||
|
||||
# Editor swap / backup files
|
||||
*~
|
||||
*.swp
|
||||
*.swo
|
||||
@@ -0,0 +1,3 @@
|
||||
# Used by shellcheck (run from the project root: shellcheck profile.sh profile.d/*.sh helpers/*.sh)
|
||||
external-sources=true
|
||||
source-path=SCRIPTDIR
|
||||
@@ -1,11 +1,29 @@
|
||||
Copyright (c) <year> <owner>.
|
||||
Copyright 2013-2026 Geoffray Levasseur <fatalerrors@geoffray-levasseur.org>
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
|
||||
This is distributed with BSD-3-Clause license with the following terms and
|
||||
condition:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
|
||||
Redistribution and use in source and binary forms, with or without modification,
|
||||
are permitted provided that the following conditions are met:
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation and/or
|
||||
other materials provided with the distribution.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
3. Neither the name of the copyright holder nor the names of its contributors
|
||||
may be used to endorse or promote products derived from this software without
|
||||
specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
|
||||
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
|
||||
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
@@ -1,3 +1,579 @@
|
||||
# profile
|
||||
|
||||
bash profile repository
|
||||
This project aims to create an advanced bash profile. It includes aliases,
|
||||
a customized prompt and several functions for different purposes. It's mostly
|
||||
targeted to system administrators but might satisfy some regular users.
|
||||
|
||||
## 1. Requirements
|
||||
|
||||
profile requires **Bash 4.3 or higher** (for associative arrays and namerefs).
|
||||
It will refuse to load on older versions and will also refuse to load if the
|
||||
current shell is not bash.
|
||||
|
||||
## 2. Getting started
|
||||
|
||||
Download and extract (or use git clone) the profile archive into your home
|
||||
directory.
|
||||
|
||||
The profile is designed to be **sourced**, not executed directly.
|
||||
|
||||
Manual setup:
|
||||
|
||||
```bash
|
||||
source <installpath>/profile/profile.sh
|
||||
```
|
||||
|
||||
Automatic setup (recommended):
|
||||
|
||||
```bash
|
||||
bash <installpath>/profile/profile.sh --install
|
||||
```
|
||||
|
||||
`--install` appends the required `source` line to both `~/.bashrc` and
|
||||
`~/.profile` by default. You can target one file only:
|
||||
|
||||
```bash
|
||||
bash <installpath>/profile/profile.sh --install --bashrc
|
||||
bash <installpath>/profile/profile.sh --install --profile
|
||||
```
|
||||
|
||||
You may also set the `PROFILE_PATH` environment variable before sourcing if you
|
||||
want to override the automatic path detection:
|
||||
|
||||
```bash
|
||||
export PROFILE_PATH=/opt/profile
|
||||
source /opt/profile/profile.sh
|
||||
```
|
||||
|
||||
It's not recommended to load that profile in `/etc/profile` as users' `.bashrc`
|
||||
files might interfere with some aliases and functions defined in profile.
|
||||
|
||||
### 2.1. Interactive vs non-interactive shells
|
||||
|
||||
`profile.sh` detects whether the current shell is interactive.
|
||||
|
||||
In interactive shells (typical terminal sessions), profile enables
|
||||
interactive-only features such as:
|
||||
|
||||
- aliases from `[aliases]`
|
||||
- bash completion scripts from `profile.d/bash-completion/`
|
||||
- prompt initialization (`PROMPT_COMMAND` and timer hook)
|
||||
- welcome display (`showinfo`) and startup update check (`check_updates -q`)
|
||||
|
||||
In non-interactive shells (typical script execution), those features are
|
||||
intentionally skipped to avoid side effects and startup noise. Public functions
|
||||
remain available after sourcing, so scripts can still call profile helpers.
|
||||
|
||||
### 2.2. Initial configuration
|
||||
|
||||
Copy the example configuration file and customise it to your needs:
|
||||
|
||||
```bash
|
||||
cp <installpath>/profile/doc/profile.conf.example <installpath>/profile/profile.conf
|
||||
```
|
||||
|
||||
`profile.conf` is git-ignored so your personal settings will never be
|
||||
accidentally committed. All keys are optional — sensible defaults apply when
|
||||
unset. See [section 4](#4-configuration) for the full reference.
|
||||
|
||||
## 3. What's the purpose?
|
||||
|
||||
profile gives access to numerous functions, aliases and to an advanced prompt.
|
||||
All functions are organized into modules under the `profile.d/` directory and
|
||||
are loaded automatically at startup.
|
||||
|
||||
The internal helpers (functions starting with `_`, not meant to be called by
|
||||
hand) are kept apart from the modules, in the `helpers/` directory:
|
||||
`helpers/common.sh` holds the ones shared by several modules (option parsing,
|
||||
trap restoration, ...), and each module loads its own private helpers from
|
||||
`helpers/<module name>.sh` when that file exists (`profile.d/rain.sh` loads
|
||||
`helpers/rain.sh`). Both are loaded automatically; `helpers/` must be kept next
|
||||
to `profile.sh` and `profile.d/`.
|
||||
|
||||
### 3.1. Prompt
|
||||
|
||||
A bar-style prompt showing current time, execution time of the last command
|
||||
(with sub-millisecond precision), and the exit code of the last command.
|
||||
|
||||
### 3.2. Functions reference
|
||||
|
||||
| Function | Module | Description |
|
||||
| --- | --- | --- |
|
||||
| `backtrace` | debug | Print the current call stack (function, file, line of each call), innermost frame first; used by the `settrace` error trace |
|
||||
| `busy` | fun | Monitor /dev/urandom for a hex pattern — look busy; the pattern can also be given as argument; runs until a key (or Ctrl-C) is pressed, needs a terminal |
|
||||
| `check_updates` | updates | Check whether a newer profile version is available online; when called with `-q` at startup a 3-second network timeout is applied so a slow or absent network never delays the prompt; the published version is validated before use and an installed version more recent than the published one is not reported as outdated |
|
||||
| `clean` | filefct | Erase backup files in given directories, optionally recursive |
|
||||
| `conf_dump` | conf | Display the profile configuration file; `-s/--section NAME` restricts output to one section; `-a/--all` also lists commented-out keys (documented defaults, skipped when the key is set); an optional pattern argument filters keys by substring match; honours `NO_COLOR` |
|
||||
| `conf_save` | conf | Save or update a key=value pair in a configuration section: `conf_save <section> <key> <value>`; replaces an existing key in place or appends it to the section (creating the section if absent); works through a symlinked file and keeps its permissions; the value must be single-line and the running shell is not updated (applies at next load) |
|
||||
| `disp` | disp | Display formatted info / warning / error / debug messages; long messages are word-wrapped and continuation lines are indented to align with the message text; honours `NO_COLOR`, `QUIET=1` (hides info messages) and `DEBUG` > 1 (shows debug messages); warnings, errors and debug go to stderr |
|
||||
| `dwl` | net | Download a URL using curl, wget, or fetch transparently; supports `-t <seconds>` / `--timeout <seconds>` to cap the transfer time |
|
||||
| `error` | debug | ERR trap handler installed by `settrace`: reports the failed command status and a call stack on stderr, then keeps the status |
|
||||
| `expandlist` | filefct | Expand glob expressions into a quoted, separated list; names with spaces are kept whole, `\`, `"`, `$` and `` ` `` are escaped, and the separator (`-s`, `-n` or `EXPANDLIST_DEFAULT_SEPARATOR`) may use escapes such as `\n` |
|
||||
| `fake_compile` | fun | Simulate a long compilation process with random warnings and errors |
|
||||
| `file_stats` | filefct | Display file size statistics for a path; `--min`/`--max` are inclusive and validated, the histogram is sorted |
|
||||
| `findbig` | filefct | Find the biggest files in the given or current directory |
|
||||
| `finddead` | filefct | Find dead symbolic links in the given or current directory |
|
||||
| `findzero` | filefct | Find empty files in the given or current directory; `--delete` does not follow symlinks |
|
||||
| `gacp` | git | Add, commit and push changes; auto-pulls with rebase first if needed |
|
||||
| `genpwd` | pwd | Generate one or more random passwords with configurable constraints, from the kernel's random generator (`$SRANDOM`, or `/dev/urandom`); only the passwords go to stdout |
|
||||
| `ggraph` | git | Display a decorated git history graph |
|
||||
| `gpid` | processes | Give the list of PIDs matching the given process name(s) |
|
||||
| `gprune` | git | Delete local branches already merged into the main branch; `-D`/`--deep` also deletes (force) branches whose upstream branch was deleted on the remote, e.g. after a merge request |
|
||||
| `greset` | git | Reset the current branch to upstream, stashing local changes first (the target is checked before anything is stashed; the previous HEAD is printed for recovery); `-x` also removes ignored files, which cannot be undone |
|
||||
| `groot` | git | Display the repository root path, or change directory to it with `-g` |
|
||||
| `gsync` | git | Fetch (the remote the branch tracks, or the given one) and rebase the current branch onto its upstream; needs a checked-out branch |
|
||||
| `gst` | git | Display compact git status with branch tracking information |
|
||||
| `gwip` | git | Create a quick WIP checkpoint commit |
|
||||
| `hack` | fun | Simulate a dramatic multi-phase hacking sequence |
|
||||
| `help` | help | Display the list of available functions and basic usage; `help <command>` delegates to `<command> --help` for shell functions (profile commands) only; honours `NO_COLOR` |
|
||||
| `isipv4` | net | Tell if the given parameter is a valid IPv4 address |
|
||||
| `isipv6` | net | Tell if the given parameter is a valid IPv6 address |
|
||||
| `kt` | processes | Kill a process and all its descendants, deepest first (kill tree); `-n`/`--dry-run`, `-s`/`--signal` or a kill-style `-9`/`-KILL`; refuses PID 0, 1 and the current shell; needs `pgrep` |
|
||||
| `ku` | processes | Kill all processes owned by the given user name or ID; `-n`/`--dry-run`, `-s`/`--signal` or a kill-style `-9`/`-KILL`; all users are checked before anything is killed; never kills root's or the current user's processes (except with `-n`) |
|
||||
| `matrix` | rain | Console screensaver with Matrix-style digital rain (binary, kana, ascii charset) |
|
||||
| `mcd` | filefct | Create a directory and immediately move into it |
|
||||
| `mdcat` | disp | Render Markdown files (or stdin) in terminal with colors, inline formatting, tables and framed code blocks; honours `NO_COLOR` |
|
||||
| `locale_check` | lang | Tell whether a locale is installed, tolerating codeset spellings (`UTF-8`/`utf8`); prints the name exactly as the system spells it |
|
||||
| `meteo` | info | Display weather forecast for the configured or given city |
|
||||
| `myextip` | net | Get information about your public IP address (IP, ISP, location, coordinates, AS, timezone) from an HTTPS lookup service; needs `jq` for the formatted output, `-R` shows the raw JSON |
|
||||
| `get_pkgmgr` | packages | Detect the active package manager of the running distribution (`apt`, `dnf`, `yum`, `zypper`, `pacman`, `apk`, `portage`, `xbps`, `nix`) |
|
||||
| `pinfo` | info | Display system information read from `/proc` and `/sys` only |
|
||||
| `pkgf` | packages | Find which installed package owns a given file (distro-aware via `get_pkgmgr`) |
|
||||
| `pkgi` | packages | Install packages with the distribution's manager (distro-aware via `get_pkgmgr`); `-y` assumes yes, `-n` only shows the command; uses `sudo` (or `doas`) when not root |
|
||||
| `pkgr` | packages | Uninstall packages; `-p`/`--purge` also deletes configuration files (apt, pacman, apk; other managers just remove), `-d`/`--deps` also removes dependencies no longer needed (`--auto-remove`, `-Rs`, `--clean-deps`, `depclean`, ... — apk always does, dnf/yum are told not to unless `-d`); `-y`, `-n` as `pkgi` |
|
||||
| `pkgs` | packages | Search for a pattern in installed package names (distro-aware via `get_pkgmgr`) |
|
||||
| `ppg` | processes | Look for the given pattern in running processes |
|
||||
| `ppn` | processes | List processes whose command name (as shown by `ps -o comm`) is exactly the given one |
|
||||
| `ppu` | processes | List processes owned by a specific user |
|
||||
| `profile_upgrade` | updates | Upgrade profile to the latest version: `git pull --ff-only` on a Git install, otherwise the downloaded (or `-f` local) archive, refused if it contains absolute or `..` paths; `-g` switches to a Git clone keeping `profile.conf` and custom themes; modules left over from an older version are listed, never deleted; `-n` shows what would be done |
|
||||
| `pwdscore` | pwd | Calculate the strength score of a given password |
|
||||
| `rain` | rain | Console screensaver with falling-rain effect (multiple color themes) |
|
||||
| `rainbow` | rain | Full-screen rainbow screensaver using only background colors, with horizontal color shifting |
|
||||
| `rmhost` | ssh | Remove host(s) from SSH known_hosts: a name is removed with every IP address it resolves to, `[host]:port` entries and glob patterns are supported; `--all-users` (root) handles every local user's file and keeps its owner and mode |
|
||||
| `rmspc` | filefct | Replace spaces in filenames with underscores (or a custom character, attached: `-c-` / `--subst-char=-`); never overwrites an existing file and does not follow symlinked directories |
|
||||
| `setc` | lang | Set locale to standard C (POSIX) |
|
||||
| `setlocale` | lang | Set console locale to any installed locale (codeset spelling tolerant, UTF-8 preferred when none is given); clears `LC_ALL` and the other per-category variables so the change always applies |
|
||||
| `set_colors` | disp | (Re)define the colour variables (`Red`, `BIWhite`, `On_Blue`, `RESETCOL`, ...) used by `disp`, the prompt and themes; run at load time |
|
||||
| `set_theme` | prompt | Switch the prompt colour theme for the session (`--list`, `--preview`, `--save`) |
|
||||
| `settrace` | debug | Activate (`--on`), deactivate (`--off`) or query (`--status`) an ERR trap that prints the exit status and a call stack (on stderr) for each failing command; `--force` replaces/removes an ERR trap set by something else; active at startup when `SETTRACE_DEFAULT_ENABLED=1` |
|
||||
| `showinfo` | info | Display welcome banner and system information (figlet + fastfetch/neofetch) |
|
||||
| `ssr` | ssh | SSH into a server as root (the server comes first, then extra ssh options); default options from `SSH_DEFAULT_OPT` |
|
||||
| `taz` | compress | Compress files and directories into a chosen archive format |
|
||||
| `term_set` | conf | Set `TERM` to the best available terminal capability |
|
||||
| `urlencode` | net | URL-encode a string |
|
||||
| `utaz` | compress | Smartly uncompress archives (zip, tar.gz/bz2/xz/lz, rar, arj, lha, ace, 7z, zst, cpio, cab, deb, rpm) |
|
||||
| `ver` | info | Display the installed profile version |
|
||||
|
||||
Locale shortcut functions (`setfr`, `setus`, etc.) are dynamically generated at
|
||||
startup from the `SET_LOCALE` configuration key (see section 4).
|
||||
|
||||
### 3.3. Bash completion
|
||||
|
||||
profile loads all `*.sh` files found under `profile.d/bash-completion/`
|
||||
automatically in interactive sessions. This directory is the right place to add
|
||||
any custom completion definitions. profile already ships completions for its git
|
||||
helper functions there (`git-completion.sh`).
|
||||
|
||||
## 4. Configuration
|
||||
|
||||
profile uses an INI-style configuration file (`profile.conf`) located in the
|
||||
same directory as `profile.sh`. Sections are declared with `[section_name]` and
|
||||
keys follow `key = value` syntax. Each module calls `load_conf "<section>"` at
|
||||
load time, which exports every key in that section as an environment variable.
|
||||
Unknown keys are silently ignored; all keys are optional — sensible defaults
|
||||
apply when unset.
|
||||
|
||||
A line is `key = value` (the key is a variable name; a line that is neither a
|
||||
section, a comment nor a valid assignment is reported and ignored). Lines starting
|
||||
with `#` or `;` are comments, and so is the end of a line after a blank and a `#`
|
||||
(`PROMPT_THEME = dark # my theme`); a value between single or double quotes is
|
||||
what is inside the quotes, so a `#` can be kept in it. `$VAR` references to
|
||||
existing environment variables are expanded; backticks and `$(` are removed.
|
||||
|
||||
`profile.conf` is listed in `.gitignore` so personal values (API keys, cities,
|
||||
compiler flags, …) are never accidentally staged. Start from the annotated
|
||||
template at `doc/profile.conf.example` (see [section 2.2](#22-initial-configuration)).
|
||||
|
||||
### 4.1. Core sections
|
||||
|
||||
| Section | Purpose |
|
||||
| --- | --- |
|
||||
| `[system]` | Bash history size, pager, terminal type, and other shell behaviours |
|
||||
| `[general]` | General-purpose variables (e.g. compilation flags, `MAKEFLAGS`) |
|
||||
| `[aliases]` | User command aliases, loaded for interactive shells only |
|
||||
|
||||
#### `TERM` — terminal type detection
|
||||
|
||||
The `TERM` key in `[system]` has special handling via `term_set`, which runs
|
||||
automatically at startup:
|
||||
|
||||
| Value | Behaviour |
|
||||
| --- | --- |
|
||||
| *(absent or `smart`)* | Auto-detect the best available `terminfo` entry (default) |
|
||||
| any other string | Exported as-is — standard Unix behaviour |
|
||||
|
||||
When auto-detection runs, `COLORTERM=truecolor|24bit` is evaluated as a global
|
||||
truecolor flag: it does not pick an entry by itself, it only enables the
|
||||
`*-direct` (24-bit colour) terminfo entries below, each of which falls back to its
|
||||
`*-256color` counterpart. Hints are checked in the following order and the first
|
||||
entry confirmed to exist via `terminfo` is used. Terminal multiplexers come first
|
||||
because they inherit the outer emulator's variables (`TERM_PROGRAM`,
|
||||
`VTE_VERSION`, …) but need their own `TERM`.
|
||||
|
||||
1. **`$TMUX`** — tmux session → `tmux-direct`¹ → `screen-direct`¹, then `tmux-256color`, then `screen-256color`
|
||||
2. **`$STY`** — GNU screen session → `screen-direct`¹, then `screen-256color`, then `screen`
|
||||
3. **`$TERM_PROGRAM`** — emulator name hints:
|
||||
- `iTerm.app` → `xterm-direct`¹, then `xterm-256color`
|
||||
- `WezTerm` → `xterm-direct`¹, then `wezterm`, then `xterm-256color`
|
||||
- `Hyper`, `vscode` → `xterm-direct`¹, then `xterm-256color`
|
||||
4. **`$VTE_VERSION`** — GNOME Terminal, Tilix, … → `vte-direct`¹, then `vte-256color`, then `xterm-256color`
|
||||
5. **`$WT_SESSION`** — Windows Terminal → `xterm-direct`¹, then `xterm-256color`
|
||||
6. **Generic probe** — `xterm-256color` → `xterm-color` → `xterm` → `vt100`
|
||||
|
||||
¹ Only attempted when the truecolor flag is set (`COLORTERM=truecolor` or `24bit`).
|
||||
|
||||
### 4.2. Module defaults
|
||||
|
||||
Each module exposes its hardcoded defaults as configuration keys. Set a key to
|
||||
change the default without having to pass flags every time.
|
||||
|
||||
**`[compress]`** — `taz` / `utaz`
|
||||
|
||||
| Key | Default | Description |
|
||||
| --- | --- | --- |
|
||||
| `TAZ_DEFAULT_FORMAT` | `lz` | Archive format(s) for `taz`: `lz`, `xz`, `bz2`, `gz`, `lzo`, `tar`; comma-separated list keeps the smallest result |
|
||||
| `TAZ_DEFAULT_THREADS` | `auto` | Compression threads (`auto` = runtime CPU count, or explicit positive integer) |
|
||||
| `TAZ_DEFAULT_LEVEL` | `6` | Compression level (1–9) |
|
||||
| `UTAZ_DEFAULT_DELETE` | `0` | Set to `1` to delete the source archive after extraction |
|
||||
| `UTAZ_DEFAULT_DIR_MODE` | `auto` | Host directory policy: `auto` (only when the archive has no single top-level directory), `always` or `never` |
|
||||
| `UTAZ_DEFAULT_IGNORE_CASE` | `0` | Set to `1` to match archive extensions case-insensitively (`.ZIP`, `.Tar.GZ`). Per call: `utaz -i` / `utaz -s` |
|
||||
|
||||
**`[debug]`** — `settrace`
|
||||
|
||||
| Key | Default | Description |
|
||||
| --- | --- | --- |
|
||||
| `SETTRACE_DEFAULT_ENABLED` | `0` | Set to `1` to activate the error trace (`settrace --on`) at the startup of every interactive shell |
|
||||
|
||||
**`[filefct]`** — file utilities
|
||||
|
||||
| Key | Default | Description |
|
||||
| --- | --- | --- |
|
||||
| `EXPANDLIST_DEFAULT_SEPARATOR` | ` ` (space) | Separator used by `expandlist` |
|
||||
| `CLEAN_DEFAULT_RECURSIVE` | `0` | Set to `1` to make `clean` recurse into subdirectories |
|
||||
| `RMSPC_DEFAULT_CHAR` | `_` | Replacement character used by `rmspc` |
|
||||
| `FINDBIG_DEFAULT_LIMIT` | `10` | Number of results returned by `findbig` |
|
||||
|
||||
**`[rain]`** — screensavers
|
||||
|
||||
| Key | Default | Description |
|
||||
| --- | --- | --- |
|
||||
| `RAIN_DEFAULT_SPEED` | `5` | Delay between frames for `rain`: a value ≥ 1 is in hundredths of a second (`5` = 0.05 s), a value < 1 is in seconds; must be > 0 |
|
||||
| `RAIN_DEFAULT_COLOR` | `white` | Colour for `rain` (`green`, `blue`, `red`, `yellow`, `cyan`, `white`; case-insensitive) |
|
||||
| `RAIN_DEFAULT_DENSITY` | dynamic | Maximum number of simultaneous falling elements for `rain` |
|
||||
| `RAINBOW_DEFAULT_SPEED` | `4` | Frame delay (hundredths of a second) of the colour shift for `rainbow` |
|
||||
| `MATRIX_DEFAULT_SPEED` | `3.5` | Frame delay for `matrix`, same scale as `RAIN_DEFAULT_SPEED` |
|
||||
| `MATRIX_DEFAULT_COLOR` | `green` | Colour for `matrix` (same names as `rain`) |
|
||||
| `MATRIX_DEFAULT_DENSITY` | dynamic | Maximum number of simultaneous falling elements for `matrix` |
|
||||
| `MATRIX_DEFAULT_CHARSET` | `binary` | Character set for `matrix` (`binary`, `kana`, `ascii`; case-insensitive) |
|
||||
|
||||
**`[ssh]`**
|
||||
|
||||
| Key | Default | Description |
|
||||
| --- | --- | --- |
|
||||
| `SSH_DEFAULT_OPT` | `-Y` when unset | Options passed to `ssr` before the server (e.g. `-Y -o StrictHostKeyChecking=accept-new`); set it empty for none |
|
||||
|
||||
**`[pwd]`** — password tools
|
||||
|
||||
| Key | Default | Description |
|
||||
| --- | --- | --- |
|
||||
| `GENPWD_DEFAULT_LENGTH` | `16` | Generated password length |
|
||||
| `GENPWD_DEFAULT_OCCURS` | `2` | Maximum number of occurrences of the same character in a password |
|
||||
| `GENPWD_DEFAULT_COUNT` | `1` | Number of passwords to generate |
|
||||
| `GENPWD_DEFAULT_SYMBOLS` | `1` | Include symbols (0/1) |
|
||||
| `GENPWD_DEFAULT_UPPERCASE` | `1` | Include uppercase letters (0/1) |
|
||||
| `GENPWD_DEFAULT_LOWERCASE` | `1` | Include lowercase letters (0/1) |
|
||||
| `GENPWD_DEFAULT_NUMBERS` | `1` | Include digits (0/1) |
|
||||
| `PWDSCORE_DEFAULT_VERBOSE` | `0` | Set to `1` for detailed scoring output from `pwdscore` |
|
||||
|
||||
**`[fun]`**
|
||||
|
||||
| Key | Default | Description |
|
||||
| --- | --- | --- |
|
||||
| `BUSY_DEFAULT_PATTERN` | `ca fe` | String searched (fixed string, case-insensitive) in the hexdump lines printed by `busy` |
|
||||
| `BUSY_DEFAULT_DELAY` | `0` | Delay between printed lines for `busy`, in milliseconds (integer, `0` = none) |
|
||||
| `FAKE_COMPILE_DEFAULT_MIN_DELAY` | `40` | Min delay between output lines for `fake_compile` (ms) |
|
||||
| `FAKE_COMPILE_DEFAULT_MAX_DELAY` | `150` | Max delay between output lines for `fake_compile` (ms) |
|
||||
| `FAKE_COMPILE_DEFAULT_LANG` | `c` | Default language preset for `fake_compile` (`c`, `cpp`, `java`, `python`, `random`) |
|
||||
| `HACK_DEFAULT_MIN_DELAY` | `60` | Min delay between output lines for `hack` (ms) |
|
||||
| `HACK_DEFAULT_MAX_DELAY` | `250` | Max delay between output lines for `hack` (ms) |
|
||||
|
||||
**`[git]`**
|
||||
|
||||
| Key | Default | Description |
|
||||
| --- | --- | --- |
|
||||
| `GIT_MAIN_BRANCH` | `main` | Fallback main branch name used when remote HEAD cannot be detected |
|
||||
| `GIT_DEFAULT_REMOTE` | `origin` | Default remote used by git helper functions |
|
||||
| `GIT_WIP_PREFIX` | `wip` | Prefix used by `gwip` when generating automatic checkpoint messages |
|
||||
| `GIT_GACP_AUTO_ADD` | `1` | Set to `1` to make `gacp` automatically add all modified files when no explicit file list is given; set to `0` to require explicit paths or the `-a` flag |
|
||||
|
||||
**`[info]`**
|
||||
|
||||
| Key | Default | Description |
|
||||
| --- | --- | --- |
|
||||
| `METEO_DEFAULT_CITY` | _(empty)_ | Default city for `meteo` when no argument is given |
|
||||
|
||||
**`[net]`**
|
||||
|
||||
| Key | Default | Description |
|
||||
| --- | --- | --- |
|
||||
| `DWL_PREFERRED_TOOL` | _(empty)_ | Force `dwl` to use `curl`, `wget`, or `fetch` (auto-detected when unset) |
|
||||
| `DWL_DEFAULT_RESUME` | `0` | Enable `dwl` resume mode by default (`1`/`true`/`yes`/`on`); applies to file downloads |
|
||||
| `MYEXTIP_DEFAULT_URL` | `https://ipinfo.io/json` | HTTPS JSON API used by `myextip`. `https://ipwho.is/` is also supported, as is the (HTTP-only on its free tier) ip-api.com format; a non-HTTPS URL triggers a warning |
|
||||
|
||||
**`[packages]`**
|
||||
|
||||
| Key | Default | Description |
|
||||
| --- | --- | --- |
|
||||
| `PKGS_DEFAULT_IGNORE_CASE` | `0` | Set to `1` to make `pkgs` case-insensitive by default |
|
||||
|
||||
**`[processes]`**
|
||||
|
||||
| Key | Default | Description |
|
||||
| --- | --- | --- |
|
||||
| `PPU_DEFAULT_FORMAT` | `pid,user,%cpu,%mem,start,time,command` | `ps` output format used by `ppu` |
|
||||
| `KU_DEFAULT_SIGNAL` | `TERM` | Default signal sent by `ku` |
|
||||
|
||||
**`[updates]`**
|
||||
|
||||
| Key | Default | Description |
|
||||
| --- | --- | --- |
|
||||
| `UPDT_DEFAULT_BRANCH` | `master` | Git branch used for update checks and `profile_upgrade` (letters, digits, `.`, `_`, `-`, `/`; anything else falls back to `master`). Changing this value causes `profile_upgrade` to automatically switch the local checkout to the new branch on the next run and display a warning. |
|
||||
|
||||
### 4.3. Locale shortcuts
|
||||
|
||||
The `[lang]` key `SET_LOCALE` accepts a comma-separated list of
|
||||
`alias:locale` pairs. Each pair generates a function of that name at startup:
|
||||
|
||||
```ini
|
||||
SET_LOCALE = fr:fr_FR.UTF-8, us:en_US.UTF-8
|
||||
```
|
||||
|
||||
This creates `setfr` and `setus`. Use `setlocale <locale>` to switch to any
|
||||
installed locale directly.
|
||||
|
||||
Set `DEFAULT_LANG` to one of the defined aliases to activate that locale
|
||||
automatically at login:
|
||||
|
||||
```ini
|
||||
DEFAULT_LANG = fr
|
||||
```
|
||||
|
||||
If `DEFAULT_LANG` is set but does not match any alias in `SET_LOCALE`, a
|
||||
warning is displayed and no locale change is applied.
|
||||
|
||||
### 4.4. Prompt theming
|
||||
|
||||
The prompt appearance is controlled by two mechanisms that are applied in order
|
||||
(later values win):
|
||||
|
||||
1. **Theme file** — sets a base colour palette.
|
||||
2. **`[prompt]` section** in `profile.conf` — per-key overrides on top of the theme.
|
||||
|
||||
**Selecting a theme:**
|
||||
|
||||
```ini
|
||||
[prompt]
|
||||
PROMPT_THEME = dark # bare name → profile.d/themes/dark.theme
|
||||
PROMPT_THEME_DIR = ~/.mythemes # optional: custom search directory
|
||||
```
|
||||
|
||||
Built-in themes: `default`, `dark`, `light`, `solarized`, `solarized-light`,
|
||||
`monokai`, `monochrome`, `abyss`, `plasma`, `adwaita`.
|
||||
|
||||
**Runtime theme switching (`set_theme`):**
|
||||
|
||||
```bash
|
||||
set_theme --list # list available themes
|
||||
set_theme dark # apply theme for current shell session
|
||||
set_theme --preview dark # preview theme colors without applying
|
||||
set_theme --save # save currently active theme to config
|
||||
set_theme --save dark # apply and save the given theme
|
||||
```
|
||||
|
||||
`set_theme --save` writes `PROMPT_THEME` in `[prompt]` to:
|
||||
|
||||
- `~/.profile.conf` when present
|
||||
- otherwise `profile.conf` in the profile installation directory
|
||||
|
||||
`--preview` and `--save` are mutually exclusive.
|
||||
|
||||
**Overriding individual prompt colour slots:**
|
||||
|
||||
```ini
|
||||
[prompt]
|
||||
PROMPT_COLOR_USER_FG = $ICyan
|
||||
PROMPT_COLOR_DIR_FG = $IYellow
|
||||
```
|
||||
|
||||
A value is one or more blocks, each a colour variable (`$ICyan`, `${ICyan}`) or a
|
||||
raw ANSI literal (`\e[1;32m`, `\033[4m`), e.g. `$BWhite\e[4m`; anything else is
|
||||
rejected with a warning (colours end up in `PS1`). These overrides always win over
|
||||
the theme, also after `set_theme`.
|
||||
|
||||
The twelve available `PROMPT_COLOR_*` keys are:
|
||||
|
||||
| Key | Role |
|
||||
| --- | --- |
|
||||
| `PROMPT_COLOR_TIME_FG` / `TIME_BG` | Clock foreground / background |
|
||||
| `PROMPT_COLOR_BAR_BG` | Main bar background |
|
||||
| `PROMPT_COLOR_OK_FG` / `OK_MARK` | Exit-code text / checkmark on success |
|
||||
| `PROMPT_COLOR_ERR_BG` / `ERR_FG` / `ERR_MARK` | Error bar background / text / X mark |
|
||||
| `PROMPT_COLOR_ROOT_FG` | Username colour when running as root |
|
||||
| `PROMPT_COLOR_USER_FG` | Username@host colour for normal users |
|
||||
| `PROMPT_COLOR_DIR_FG` | Working directory colour |
|
||||
| `PROMPT_COLOR_CTX_FG` | Git/Conda context segment colour at end of top bar |
|
||||
|
||||
**Top-bar context segment (Git / Conda):**
|
||||
|
||||
```ini
|
||||
[prompt]
|
||||
PROMPT_SHOW_GIT = 1
|
||||
PROMPT_SHOW_GIT_STATUS = 1
|
||||
PROMPT_GIT_TIMEOUT = 2
|
||||
PROMPT_SHOW_CONDA = 1
|
||||
PROMPT_SHOW_VENV = 1
|
||||
PROMPT_SHOW_SESSION = 1
|
||||
```
|
||||
|
||||
When enabled, the top prompt bar appends:
|
||||
|
||||
- `git:<branch>` when inside a Git repository
|
||||
- `git:<branch>*` when local changes are present
|
||||
- `git:<branch> +N/-M` when ahead/behind upstream
|
||||
- `git:<branch>?` when `git diff` or `git rev-list` exceeded `PROMPT_GIT_TIMEOUT`
|
||||
- `conda:<env>` when a Conda environment is active
|
||||
- `venv:<name>` when a Python virtualenv is active (and Conda is not)
|
||||
- `ssh`, `tmux`, `screen` markers when the session runs in those contexts
|
||||
- all, separated by `|`, when several are available
|
||||
|
||||
**Writing a custom theme file:**
|
||||
|
||||
Theme files live in `profile.d/themes/` and use the `.theme` extension. They
|
||||
are **parsed, not executed** — no shell code runs. Only two value forms are
|
||||
accepted:
|
||||
|
||||
```ini
|
||||
# Colour variable reference (resolved from profile.d/disp.sh)
|
||||
PROMPT_COLOR_DIR_FG = "$ICyan"
|
||||
|
||||
# Raw ANSI escape sequence (single block, 16-colour or 24-bit true-colour)
|
||||
PROMPT_COLOR_BAR_BG = "\e[48;2;7;54;66m"
|
||||
```
|
||||
|
||||
Any unknown key, unsafe value, or shell construct is discarded with a warning.
|
||||
Theme files may also override the standard colour variables (`Black`, `Blue`,
|
||||
`On_IBlack`, etc.) to remap the entire terminal palette used by `ls`, `grep`,
|
||||
and other colour-aware tools.
|
||||
|
||||
True-colour themes (`solarized`, `solarized-light`) require a terminal with
|
||||
24-bit colour support (Konsole, iTerm2, kitty, Alacritty, Windows Terminal).
|
||||
Verify support with:
|
||||
|
||||
```bash
|
||||
printf '\e[38;2;38;139;210mTrue colour test\e[0m\n'
|
||||
```
|
||||
|
||||
## 5. Optional dependencies
|
||||
|
||||
profile is designed so that every external dependency is optional: all features
|
||||
degrade gracefully (an error message is shown and the specific function returns
|
||||
early) when a binary is absent. The table below lists packages that are **not**
|
||||
part of a standard minimal Linux installation (might differ from your actual
|
||||
distribution, some are still extremely common). Nothing in this list is required
|
||||
to load profile or use its core functions.
|
||||
|
||||
| Binary | Typical package name | Distr. | Function(s) | Behaviour when absent |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| `figlet` | `figlet` | all | `showinfo` | ASCII banner skipped |
|
||||
| `neofetch` | `neofetch` | all | `showinfo` | Used only if `fastfetch` is absent; then falls back to the built-in `pinfo` |
|
||||
| `fastfetch` | `fastfetch` | all | `showinfo` | Preferred over `neofetch`; if both are absent, falls back to the built-in `pinfo` |
|
||||
| `jq` | `jq` | all | `myextip` | Raw JSON shown instead of formatted output |
|
||||
| `hexdump` | `util-linux` / `bsdmainutils` | all | `busy` | Error message, function returns 1 |
|
||||
| `numfmt` | `coreutils` ≥ 8.21 | all | `file_stats --min/--max` | Error message, function returns 1 |
|
||||
| `envsubst` | `gettext` | all | config loading | `$VAR` references in `profile.conf` values left literal |
|
||||
| `killall` | `psmisc` | all | `ku` | Falls back to `pkill` (procps); error if both absent |
|
||||
| `pkill` / `pgrep` | `procps` / `procps-ng` | all | `ku`, `ppg`, `gpid` | `ku` falls back to `killall`; `ppg`/`gpid` fall back to `ps + awk`; `kt` requires `pgrep` |
|
||||
| `pigz` | `pigz` | all | `taz` (gzip) | Falls back to `gzip` (slower, single-thread) |
|
||||
| `plzip` | `plzip` | all | `taz` (lzip) | Falls back to `lzip` |
|
||||
| `lzip` / `plzip` | `lzip` | all | `taz`/`utaz` (.lz) | Error message if neither is available |
|
||||
| `xz` | `xz-utils` / `xz` | all | `taz`/`utaz` (.xz) | Error message |
|
||||
| `unrar` | `unrar` / `unrar-free` | all | `utaz` (.rar) | Error message |
|
||||
| `unarj` | `arj` | all | `utaz` (.arj) | Error message |
|
||||
| `lha` | `lhasa` | all | `utaz` (.lzh) | Error message |
|
||||
| `unace` | `unace` | all | `utaz` (.ace) | Error message |
|
||||
| `7z` | `p7zip-full` | all | `taz`/`utaz` (.7z) | Error message |
|
||||
| `cabextract` | `cabextract` | all | `utaz` (.cab) | Error message |
|
||||
| `rpm2cpio` | `rpm` | all | `utaz` (.rpm) | Error message |
|
||||
| `nix-locate` | `nix-index` | NixOS | `pkgf` | Error: unsupported package manager |
|
||||
| `qfile` | `gentoolkit` | Gentoo | `pkgf` | Error: unsupported package manager |
|
||||
|
||||
## 6. Contact and more information
|
||||
|
||||
### 6.1. New users
|
||||
|
||||
If you use (or plan to use) `profile`, I'll be happy if you simply mail me to
|
||||
let me know, especially if you don't plan to contribute. If you plan to
|
||||
contribute, I'll be twice happier for sure!
|
||||
|
||||
### 6.2. Bugs
|
||||
|
||||
**profile** bug tracker is hosted on its Gitea instance. Check the
|
||||
<https://git.geoffray-levasseur.org/fatalerrors/profile> page. If you find a bug,
|
||||
you can also submit a bug report to the maintainer mail address mentioned at
|
||||
the end of that document. A bug report may contain the command line parameters
|
||||
where the bug happens, OS details, the module that triggered it, if any, and the
|
||||
log file containing the error. Cygwin users: please note that the bash
|
||||
implementation in Cygwin regularly triggers bugs on advanced code that works
|
||||
fine on Linux or BSD. Please do not send syntax error bug reports if you
|
||||
have not tested the same code under a real Unix environment.
|
||||
|
||||
Check the [FAQ](./doc/FAQ.md) and the [to-do list](./doc/todo.md) before
|
||||
sending any feature request or bug report, as it might already be documented.
|
||||
|
||||
### 6.3. How to contribute?
|
||||
|
||||
You are free to improve and contribute as you wish. If you have no idea what to
|
||||
do or want some direction, you can check the [to-do list](./doc/todo.md),
|
||||
containing desired future improvements. Make sure you always have the latest
|
||||
development version before starting your work.
|
||||
|
||||
Read [CONTRIBUTING.md](./doc/CONTRIBUTING.md) for code style conventions,
|
||||
branch workflow, and how to submit a patch or pull request.
|
||||
|
||||
It's heavily recommended to use git to obtain the latest copy of the profile
|
||||
tree. Make sure your git configuration is correct in order to contribute.
|
||||
Please contact me to obtain push authorizations, or, if you want to submit a
|
||||
patch, you can send it by mail to the maintainer of profile.
|
||||
|
||||
Code written in Python or Perl might be accepted as long as it is not
|
||||
mobilizing a lot of dependencies (forget big frameworks). Anything that
|
||||
requires installing packages not provided in a minimal Debian or CentOS
|
||||
installation will probably be rejected.
|
||||
|
||||
If you want to make a financial contribution, please contact me by mail.
|
||||
|
||||
### 5.4. License, website, and maintainer
|
||||
|
||||
Everything except configuration files is licensed under the BSD-3 license.
|
||||
Please check the license file alongside this one.
|
||||
|
||||
Please check [https://www.geoffray-levasseur.org/profile](https://www.geoffray-levasseur.org/profile).
|
||||
|
||||
You can mail the author at fatalerrors \<at\> geoffray-levasseur \<dot\> org.
|
||||
|
||||
-----------------------------------------------------------------------------
|
||||
|
||||
Documentation (c) 2021-2026 Geoffray Levasseur.
|
||||
|
||||
This file is distributed under the 3-clause BSD license. The complete license
|
||||
agreement can be obtained at: <https://opensource.org/licenses/BSD-3-Clause>
|
||||
|
||||
@@ -0,0 +1,387 @@
|
||||
# Changelog
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
The format follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
||||
Versions follow `MAJOR.MINOR.PATCH-REVISION_STAGE_N` (e.g. `3.99.1-4_rc_1`).
|
||||
|
||||
---
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
---
|
||||
|
||||
## [4.2.0] — 2026-10-13
|
||||
|
||||
Full review of every module and of `profile.sh`: many bug fixes, comments and
|
||||
documentation updated.
|
||||
|
||||
### Added
|
||||
|
||||
- `pkgi` (install) and `pkgr` (uninstall; `-p`/`--purge`, `-d`/`--deps`).
|
||||
- `utaz -i`/`--ignore-case` and `-s`/`--case-sensitive`, default set by
|
||||
`UTAZ_DEFAULT_IGNORE_CASE`; `.zst` and `.tzst` support.
|
||||
- `SETTRACE_DEFAULT_ENABLED` key (`[debug]` section).
|
||||
- `--help` for `backtrace`, `locale_check` and `get_pkgmgr`; support functions
|
||||
added to the README table and to `help`.
|
||||
|
||||
### Changed
|
||||
|
||||
- Internal helpers moved to the new `helpers/` directory: `common.sh` (shared)
|
||||
and `<module>.sh` (private to a module, loaded by the module itself).
|
||||
- Option parsing and trap restoration no longer use `xargs` or `eval`
|
||||
(`_getopt_split`, `_trap_restore`).
|
||||
- `myextip` uses an HTTPS service (`ipinfo.io`) by default.
|
||||
- `showinfo` prefers `fastfetch` over `neofetch`.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Security: prompt injection through git branch / environment names, `genpwd`
|
||||
weak random source, unsafe handling of values read from the network or from
|
||||
the configuration.
|
||||
- Many functional bugs in `taz`/`utaz`, `conf_save`/`conf_dump`, `term_set`,
|
||||
`disp`/`mdcat`, `findzero`, `gprune`, `setlocale`, `dwl`, `ku`/`kt`, `pkgf`,
|
||||
`rmhost`, `rain`/`matrix`, `profile_upgrade` and the prompt exit status.
|
||||
- `profile.sh`: shell check, option handling when sourced, `PATH` and
|
||||
`PROMPT_COMMAND` handling, configuration parsing.
|
||||
- Debug traces go to stderr; `((x++))` patterns no longer fail under `set -e`.
|
||||
- Copyright years and license headers; the whole project is now `shellcheck`
|
||||
clean (`.shellcheckrc` added).
|
||||
|
||||
---
|
||||
|
||||
## [4.1.0] — 2026-05-07
|
||||
|
||||
### Added
|
||||
|
||||
- `profile.sh --install` command to automatically configure profile loading in
|
||||
shell startup files.
|
||||
- `--install --bashrc` and `--install --profile` target selectors for
|
||||
single-file installation.
|
||||
- **`git.sh`** — entirely new module providing git workflow helpers: `gst`,
|
||||
`ggraph`, `gsync`, `gacp`, `greset`, `gwip`, `gprune`, `groot`.
|
||||
- Dedicated git helper completions under `profile.d/bash-completion/`, loaded
|
||||
automatically in interactive sessions from `profile.d/bash-completion/*.sh`.
|
||||
|
||||
### Changed
|
||||
|
||||
- `disp` now wraps long messages on terminal width, avoids mid-word splits, and
|
||||
aligns continuation lines with the message body after the prefix.
|
||||
- `help` now supports `help <command>` and delegates to `<command> --help`.
|
||||
- `taz` now supports `-p auto` / `--parallel=auto` to automatically use the
|
||||
runtime CPU count. This mode is now the default via
|
||||
`TAZ_DEFAULT_THREADS=auto`. Backward compatibility with 0 being interpreted as
|
||||
auto is maintained.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Startup responsiveness improved: `check_updates -q` now uses a short network
|
||||
timeout so unavailable/slow networks no longer delay prompt readiness.
|
||||
- `dwl` gained timeout support (`-t` / `--timeout`) and is now used by quiet
|
||||
startup update checks to enforce fast failure.
|
||||
- `profile.sh` now detects direct execution and warns that it is designed to be
|
||||
sourced.
|
||||
|
||||
## [4.0.0] — 2026-04-23
|
||||
|
||||
### Added
|
||||
|
||||
- New `profile.conf` reference template at `doc/profile.conf.example`.
|
||||
- Dynamic locale shortcuts generated from `SET_LOCALE` and startup default
|
||||
language selection through `DEFAULT_LANG`.
|
||||
- Prompt theming system with bundled themes (`default`, `dark`, `light`,
|
||||
`solarized`, `solarized-light`, `monokai`, `monochrome`, `abyss`, `plasma`,
|
||||
`adwaita`) and per-key prompt color overrides.
|
||||
- Module defaults exposed as configuration keys in `profile.conf`.
|
||||
|
||||
### Changed
|
||||
|
||||
- `utaz` now supports a wider range of archive formats.
|
||||
- Prompt and theme rendering improved, including better 24-bit color support.
|
||||
- Overall code quality and maintainability improved across modules.
|
||||
|
||||
### Documentation
|
||||
|
||||
- README updated with full function reference and configuration tables.
|
||||
- New and expanded docs in `doc/` (`CONTRIBUTING.md`, `FAQ.md`, `todo.md`).
|
||||
- Historical releases imported from `history.txt` into this changelog.
|
||||
|
||||
---
|
||||
|
||||
## [3.99.2-4_rc_2] — 2026-04-21
|
||||
|
||||
### Fixed
|
||||
|
||||
- **`prompt.sh`** — `\$Last_Command` in PS1 was escaped, preventing the exit
|
||||
code from ever appearing in the prompt (the local variable no longer exists
|
||||
when PS1 is rendered by bash). Removed the backslash so the value is embedded
|
||||
at `set_prompt` build time.
|
||||
- **`filefct.sh` — `file_stats()`** — a stray unconditional `shift` after
|
||||
`esac` doubled-shifted arguments already shifted by each `case` branch;
|
||||
successive options such as `-H -d` were silently skipped.
|
||||
- **`packages.sh` — `pkgs()`** — replaced the unreliable binary-presence test
|
||||
(`command -v dpkg / rpm`) with the new `get_pkgmgr` function. Also corrected
|
||||
a typo in the "no package manager" error message (`avialable` → `available`).
|
||||
- **`processes.sh` — `kt()`** — copy-paste error: usage error message read
|
||||
`"Usage: ppg <string>"` instead of `"Usage: kt <pid>"`.
|
||||
|
||||
### Added
|
||||
|
||||
- **`packages.sh` — `get_pkgmgr()`** — new exported helper that detects the
|
||||
active package manager of the running distribution. Detection first reads
|
||||
`/etc/os-release` (`ID` then `ID_LIKE`), then falls back to a
|
||||
fixed-priority binary scan. Supported families: `apt`, `dnf`, `yum`,
|
||||
`zypper`, `pacman`, `apk`, `portage`, `xbps`, `nix`. Returns 1 when
|
||||
nothing is identified. Available to all future commands in `packages.sh`.
|
||||
|
||||
---
|
||||
|
||||
## [3.99.1-4_rc_1] — 2026
|
||||
|
||||
### Added
|
||||
|
||||
- **Theming system** — `load_theme` in `profile.d/prompt.sh` loads `.theme`
|
||||
files from `profile.d/themes/` (or a custom directory set via
|
||||
`PROMPT_THEME_DIR`). Theme files are **parsed, not executed** — no shell code
|
||||
ever runs from a theme.
|
||||
- **Built-in themes** — `default`, `dark`, `light`, `solarized` (24-bit
|
||||
true-colour), `solarized-light` (24-bit), `monokai`, `monochrome`, `abyss`,
|
||||
`plasma`, `adwaita`.
|
||||
- **`[prompt]` section** — `PROMPT_THEME` and `PROMPT_THEME_DIR` keys to select
|
||||
a theme; eleven `PROMPT_COLOR_*` keys for per-key overrides in `profile.conf`.
|
||||
- **Standard colour variables as theme keys** — theme files may also override
|
||||
the `disp.sh` colour variables (`Black`, `Blue`, `On_IBlack`, …) to remap the
|
||||
terminal palette used by `ls`, `grep`, and colour-aware tools.
|
||||
- **Module configuration defaults** — all modules now expose their hardcoded
|
||||
defaults as `profile.conf` keys: `TAZ_*`, `UTAZ_*`, `EXPANDLIST_*`,
|
||||
`CLEAN_*`, `RMSPC_*`, `FINDBIG_*`, `RAIN_*`, `MATRIX_*`, `SSH_DEFAULT_OPT`,
|
||||
`GENPWD_*`, `PWDSCORE_*`, `BUSY_*`, `METEO_DEFAULT_CITY`, `DWL_*`,
|
||||
`MYEXTIP_*`, `PKGS_*`, `PPU_*`, `KU_*`, `UPDT_DEFAULT_BRANCH`.
|
||||
- **`UPDT_DEFAULT_BRANCH`** — configures the git branch used by `check_updates`
|
||||
and `profile_upgrade`. Changing the key causes `profile_upgrade` to
|
||||
automatically switch the local checkout to the new branch and display a
|
||||
warning.
|
||||
- **`doc/` directory** — `CONTRIBUTING.md`, `FAQ.md`, `profile.conf.example`
|
||||
(annotated template), `todo.md`.
|
||||
- **`.gitignore`** — `profile.conf` is now git-ignored so personal settings are
|
||||
never accidentally staged.
|
||||
|
||||
### Changed
|
||||
|
||||
- README §2 now explains how to create `profile.conf` from
|
||||
`doc/profile.conf.example` (new section 2.1 "Initial configuration").
|
||||
- README §4 updated with full module-defaults tables, theming reference, and a
|
||||
note about `profile.conf` being git-ignored.
|
||||
- Theme values no longer carry the `export` keyword (they are not shell
|
||||
variables, only data).
|
||||
|
||||
### Security
|
||||
|
||||
- `load_theme` uses a strict allowlist (no `eval`, no sourcing). Only
|
||||
`PROMPT_COLOR_*` keys and known `disp.sh` colour variable names are accepted.
|
||||
Values must match `\$[A-Za-z_][A-Za-z0-9_]*` or `\\e\[[0-9;]*m`; any other
|
||||
value is discarded with a warning.
|
||||
|
||||
---
|
||||
|
||||
## [3.95.3-4_beta_3] — 2024
|
||||
|
||||
### Added
|
||||
|
||||
- Initial public release candidate series.
|
||||
- Core modules: `compress`, `disp`, `filefct`, `fun`, `help`, `info`, `lang`,
|
||||
`net`, `packages`, `processes`, `prompt`, `pwd`, `rain`, `ssh`, `updates`.
|
||||
- Bar-style prompt with time, command duration, and exit code.
|
||||
- `taz` / `utaz` archive helpers.
|
||||
- `genpwd` / `pwdscore` password tools.
|
||||
- `matrix` / `rain` screensavers.
|
||||
- `profile_upgrade` with git and archive download support.
|
||||
|
||||
---
|
||||
|
||||
> **Note:** The section below was imported from `history.txt` to preserve
|
||||
> pre-`3.95.x-4_beta` release notes.
|
||||
|
||||
## Legacy releases (imported from `history.txt`)
|
||||
|
||||
### [3.6.1] — 2026-03-05
|
||||
|
||||
- Fix typo in `compress.sh`.
|
||||
|
||||
### [3.6.0] — 2026-03-05
|
||||
|
||||
- Improved `utaz` with broader multi-format support.
|
||||
- Introduced `ppu` and `ppn`.
|
||||
- Improved update system.
|
||||
|
||||
### [3.5.0] — 2026-03-04
|
||||
|
||||
- `rain` now has configurable speed and color.
|
||||
- `showinfo` adapted to `fastfetch` (in addition to `neofetch`).
|
||||
|
||||
### [3.3.1] — 2022-02-24
|
||||
|
||||
- Fixed version detection.
|
||||
- Added `busy`.
|
||||
- Fixed use of library functions before loading.
|
||||
|
||||
### [3.3.0] — 2022-11-28
|
||||
|
||||
- Initial version update support.
|
||||
- Changed versioning code.
|
||||
- Added installation path detection.
|
||||
|
||||
### [3.2.3] — 2022-11-28
|
||||
|
||||
- Improved README.
|
||||
|
||||
### [3.2.2] — 2022-11-21
|
||||
|
||||
- Fixed `taz` compression level parsing.
|
||||
- Fixed typo in `dpkgs`.
|
||||
|
||||
### [3.2.1] — 2022-11-20
|
||||
|
||||
- Fixed several messages.
|
||||
- Made `dpkgs` RPM-aware (initial support).
|
||||
- Removed version history from main script and reverted declaration order.
|
||||
- Added required license information in all files.
|
||||
- Completed `LICENSE` file.
|
||||
|
||||
### [3.2.0] — 2022-11-18
|
||||
|
||||
- Created `disp` command and integrated it across the codebase.
|
||||
|
||||
### [3.1.1] — 2022-11-10
|
||||
|
||||
- `genpwd`: added feasibility check for requested password constraints.
|
||||
|
||||
### [3.1.0] — 2022-11-08
|
||||
|
||||
- Added password generator.
|
||||
|
||||
### [3.0.1] — 2022-11-07
|
||||
|
||||
- Added concatenation option to `rmspc`.
|
||||
- Added `ku`.
|
||||
- Improved error handling in `meteo`.
|
||||
|
||||
### [3.0.0] — 2022-08-27
|
||||
|
||||
- Split code into several files/modules.
|
||||
- Added `rain` screensaver.
|
||||
|
||||
### [2.8.2] — 2022-07-29
|
||||
|
||||
- Added warning for non-bash users.
|
||||
|
||||
### [2.8.1] — 2022-07-19
|
||||
|
||||
- Cleanup, fixes and optimizations.
|
||||
|
||||
### [2.8.0] — 2022-06-24
|
||||
|
||||
- Added `backtrace`, `error` and `settrace`.
|
||||
- Bugfixes in `showinfo`.
|
||||
|
||||
### [2.7.1] — 2022-06-22
|
||||
|
||||
- Minor corrections.
|
||||
- Added `help` command.
|
||||
|
||||
### [2.7.0] — 2022-06-21
|
||||
|
||||
- Added `isipv4` and `isipv6`, integrated into `rmhost`.
|
||||
- Removed broken Konsole save/restore support.
|
||||
|
||||
### [2.6.3] — 2021-10-18
|
||||
|
||||
- Changed PS1 to status-bar style.
|
||||
- Minor improvements.
|
||||
|
||||
### [2.6.2] — 2021-02-26
|
||||
|
||||
- Bugfix in `taz` for directories with trailing slash.
|
||||
|
||||
### [2.6.1] — 2020-12-25
|
||||
|
||||
- Added checks in `rmhost`.
|
||||
- Improved `rmspc`.
|
||||
- Created `expandlist`.
|
||||
|
||||
### [2.6.0] — 2020-10-24
|
||||
|
||||
- Added Konsole session save/restore.
|
||||
|
||||
### [2.5.3] — 2020-09-11
|
||||
|
||||
- Added aliases, improved code consistency and fixed typos.
|
||||
- Improved `utaz`, removed `showdiskmap`, removed remaining French text.
|
||||
- Added license information for future publication.
|
||||
|
||||
### [2.5.2] — 2020-03-06
|
||||
|
||||
- Sorted and improved aliases.
|
||||
|
||||
### [2.5.1] — 2020-03-05
|
||||
|
||||
- Language consistency fixes.
|
||||
- Added `pigz` support in `taz`.
|
||||
|
||||
### [2.5.0] — 2020-03-03
|
||||
|
||||
- Added `taz` and `rmspc`.
|
||||
- Renamed `auzip` to `utaz` and improved it.
|
||||
|
||||
### [2.4.0] — 2020-03-02
|
||||
|
||||
- Added `auzip`.
|
||||
|
||||
### [2.3.2] — 2020-01-31
|
||||
|
||||
- `figlet`: changed default font to `ansi_shadow`.
|
||||
|
||||
### [2.3.1] — 2020-01-16
|
||||
|
||||
- Bugfix: non-interactive shells were blocked by some functions.
|
||||
|
||||
### [2.3.0] — 2020-01-08
|
||||
|
||||
- Added `figlet` and `neofetch` as MOTD replacement.
|
||||
|
||||
### [2.2.0] — 2019-12-16
|
||||
|
||||
- Added `showinfo`.
|
||||
- First implementation of `showdiskmap`.
|
||||
|
||||
### [2.1.2] — 2019-09-24
|
||||
|
||||
- Bugfix in profile version display.
|
||||
|
||||
### [2.1.1] — 2019-09-23
|
||||
|
||||
- Bugfix in `dpkgs`.
|
||||
|
||||
### [2.1.0] — 2018-09-16
|
||||
|
||||
- Added `rmhost`, `setc`, `setfr`.
|
||||
- Improved locale management.
|
||||
|
||||
### [2.0.1] — 2017-02-04
|
||||
|
||||
- `clean` improvements (`--shell`).
|
||||
|
||||
### [2.0.0] — 2015-10-24
|
||||
|
||||
- Added advanced functions (`clean`, `ssr`, etc.).
|
||||
|
||||
### [1.0.0] — 2013-02-16
|
||||
|
||||
- Initial version.
|
||||
|
||||
### [Initial fork]
|
||||
|
||||
Forked default Bash profile from Beyond Linux From Scratch by:
|
||||
|
||||
- James Robertson <jameswrobertson@earthlink.net>
|
||||
- Dagmar d'Surreal <rivyqntzne@pbzpnfg.arg>
|
||||
@@ -0,0 +1,259 @@
|
||||
# Contributing to profile
|
||||
|
||||
Thank you for your interest in contributing. This document explains how to get
|
||||
set up, what the conventions are, and how to submit work.
|
||||
|
||||
---
|
||||
|
||||
## 1. Before you start
|
||||
|
||||
- Check the [to-do list](./todo.md) to see if your idea is already planned.
|
||||
- Check the [issue tracker](https://git.geoffray-levasseur.org/fatalerrors/profile/issues)
|
||||
to avoid duplicate work.
|
||||
- For significant changes, open an issue or contact the maintainer before
|
||||
writing code — alignment on design saves everyone time.
|
||||
|
||||
---
|
||||
|
||||
## 2. Getting the source
|
||||
|
||||
A Git clone is mandatory for contributions:
|
||||
|
||||
```bash
|
||||
git clone https://git.geoffray-levasseur.org/fatalerrors/profile.git
|
||||
cd profile
|
||||
```
|
||||
|
||||
Always work from the **latest commit on `master`** (or the branch you intend
|
||||
to target). Stale forks cause avoidable merge conflicts.
|
||||
|
||||
---
|
||||
|
||||
## 3. Branch policy
|
||||
|
||||
| Branch | Purpose |
|
||||
|---|---|
|
||||
| `master` | Main development branch — new features and enhancements go here |
|
||||
| `<version>` (e.g. `3.x`) | Maintenance branch for a released version — bugfixes backported from `master` |
|
||||
|
||||
**New functionality** must always target `master`.
|
||||
|
||||
**Bugfixes** must target the branch where the bug was introduced:
|
||||
|
||||
- If the bug exists in a released version, open the fix against that version's
|
||||
maintenance branch first, then cherry-pick onto `master`.
|
||||
- If the bug is only in `master` (unreleased), fix it directly on `master`.
|
||||
- During a release-candidate cycle, bugfixes go on the `x.*` branch and are
|
||||
merged back into `master` before the final release.
|
||||
|
||||
Do **not** add new features to a maintenance branch.
|
||||
|
||||
---
|
||||
|
||||
## 4. Versioning scheme
|
||||
|
||||
Versions follow the format **`MAJOR.MINOR.PATCH`** where the `MINOR` number
|
||||
conveys the development stage of the next major release:
|
||||
|
||||
| Minor range | Stage | Rules |
|
||||
|---|---|---|
|
||||
| `x.90.y` | **Alpha** toward `x+1` | Stays on `master`. Development is open: new features are welcome, regressions are acceptable. |
|
||||
| `x.95.y` | **Beta** toward `x+1` | The `x+1.*` maintenance branch is created at this point. No regression unless absolutely necessary; new features still allowed. |
|
||||
| `x.99.y` | **RC** toward `x+1` | Bugfixes only. No new features. No regression allowed. Becomes `x+1.0.0` when stable. |
|
||||
|
||||
Examples: `3.90.1` is the first alpha toward `4.0`, `3.99.2` is the second
|
||||
release candidate for `4.0`.
|
||||
|
||||
The `PATCH` number increments freely within a stage. A bump in `MINOR`
|
||||
(e.g. `90` → `95`) always indicates a stage promotion in development phase.
|
||||
|
||||
Any experimental version must have it's dedicated branch.
|
||||
|
||||
---
|
||||
|
||||
## 5. Development environment
|
||||
|
||||
| --- | --- | --- |
|
||||
| Bash | 4.3 | Namerefs (`local -n`) required |
|
||||
| shellcheck | any recent | Run before every commit |
|
||||
| git | any | For contributing patches |
|
||||
| bats-core | 1.x | Optional — for running the test suite |
|
||||
|
||||
Install shellcheck:
|
||||
|
||||
```bash
|
||||
# Debian / Ubuntu
|
||||
apt-get install shellcheck
|
||||
|
||||
# Fedora / RHEL
|
||||
dnf install ShellCheck
|
||||
|
||||
# macOS
|
||||
brew install shellcheck
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. Code style
|
||||
|
||||
### General rules
|
||||
|
||||
- **Bash only** — no external interpreters in core modules. Python or Perl is
|
||||
acceptable for completely self-contained, optional utilities that have no
|
||||
dependencies beyond a minimal Debian or CentOS installation.
|
||||
- **4-space indentation** — no tabs.
|
||||
- **`[[ … ]]`** for all conditionals — not `[ … ]`.
|
||||
- **`(( … ))`** for arithmetic — not `$(( … ))` in conditionals.
|
||||
- **`local`** for all function-internal variables — avoid polluting the
|
||||
environment. Prefer upper case for global and lowercase for local.
|
||||
- **`printf`** instead of `echo` all the time.
|
||||
- **Never `eval`** — use namerefs (`local -n`), `${!varname}` indirection, or
|
||||
`declare -g` instead. The only exception is the function generator of
|
||||
`build_locale_shortcuts` (`profile.d/lang.sh`): bash cannot create a function
|
||||
whose name is only known at run time otherwise, and the only value injected
|
||||
into it is quoted with `printf %q`.
|
||||
- **No hardcoded defaults** — wire every configurable value through
|
||||
`${VAR:-default}` and document the key in `profile.conf` and `README.md §4`.
|
||||
|
||||
### Function conventions
|
||||
|
||||
- Public functions **must** be exported: `export -f funcname`.
|
||||
- Every public function **must** support `-h` / `--help` and print usage to
|
||||
stdout, returning 0.
|
||||
- Use `getopt` (not `getopts`) for option parsing — it handles long options and
|
||||
`--` correctly.
|
||||
Rebuild the arguments from its output with the `_getopt_split` helper
|
||||
(`helpers/common.sh`), never with `eval set --` or `xargs`:
|
||||
|
||||
```bash
|
||||
PARSED=$(getopt -o hv --long help,verbose -n 'mycmd' -- "$@") || return 1
|
||||
local -a _pargs=()
|
||||
_getopt_split _pargs "$PARSED"
|
||||
set -- "${_pargs[@]}"
|
||||
```
|
||||
To restore traps saved with `trap -p`, use `_trap_restore` (no `eval` either).
|
||||
- Follow existing error-return conventions: 0 = success, 1 = usage error,
|
||||
2 = bad options, 3 = missing dependency, 4+ = runtime failure.
|
||||
- Prefix all local helper variables with a short unique prefix (e.g. `_taz_`)
|
||||
to prevent collisions with caller-scope variables.
|
||||
|
||||
### Module structure
|
||||
|
||||
Every new module should follow this pattern:
|
||||
|
||||
```bash
|
||||
#!/usr/bin/env bash
|
||||
# <copyright block identical to existing modules>
|
||||
|
||||
load_conf "<module_name>"
|
||||
|
||||
# --- functions ---
|
||||
|
||||
export -f my_function
|
||||
|
||||
# EOF
|
||||
```
|
||||
|
||||
Add the `load_conf` call near the top after any variable declarations.
|
||||
|
||||
### Helpers
|
||||
|
||||
Internal functions (prefixed with `_`) do not belong to the module file; they
|
||||
live in the `helpers/` directory:
|
||||
|
||||
- `helpers/common.sh`: helpers used by several modules (`_getopt_split`,
|
||||
`_trap_restore`, `_is_uint`, `_profile_load_helpers`). It is loaded by
|
||||
`profile.sh` before any module. Put a helper there as soon as a second module
|
||||
needs it, instead of copying it.
|
||||
- `helpers/<module name>.sh`: helpers private to `profile.d/<module name>.sh`
|
||||
(for instance `helpers/rain.sh` for `profile.d/rain.sh`). The file is
|
||||
optional; the module loads it with the two lines below, which also load
|
||||
`common.sh` when the module is sourced alone:
|
||||
|
||||
```bash
|
||||
declare -F _profile_load_helpers >/dev/null || . "$(dirname -- "${BASH_SOURCE[0]}")/../helpers/common.sh"
|
||||
_profile_load_helpers "${BASH_SOURCE[0]}"
|
||||
```
|
||||
|
||||
Helper files use the same copyright block as the modules and are only
|
||||
definitions (no side effect besides the variables the helpers need).
|
||||
Document each helper with a comment (purpose, `Usage:` line).
|
||||
|
||||
---
|
||||
|
||||
## 7. Configuration keys
|
||||
|
||||
When adding a configurable default:
|
||||
|
||||
1. Use `${MY_VAR:-hardcoded_default}` in the function body.
|
||||
2. Add a commented-out entry with a description to `profile.conf`.
|
||||
3. Document the key in the matching table in `README.md §4.2`.
|
||||
|
||||
---
|
||||
|
||||
## 8. Theming
|
||||
|
||||
New theme files go in `profile.d/themes/` with a `.theme` extension.
|
||||
They are **parsed, not executed** — do not add shell logic.
|
||||
See the existing themes and `README.md §4.4` for the allowed syntax.
|
||||
|
||||
---
|
||||
|
||||
## 9. Running shellcheck
|
||||
|
||||
```bash
|
||||
shellcheck profile.sh profile.d/*.sh helpers/*.sh profile.d/bash-completion/*.sh
|
||||
```
|
||||
|
||||
The `.shellcheckrc` at the root of the project makes shellcheck follow the
|
||||
sourced files (`external-sources`, `source-path=SCRIPTDIR`). The run must report
|
||||
nothing: all warnings must be resolved before a patch will be accepted. Accepted
|
||||
suppression directives (`# shellcheck disable=SCxxxx`) require an inline
|
||||
comment explaining why the suppression is necessary.
|
||||
|
||||
---
|
||||
|
||||
## 10. Submitting a contribution
|
||||
|
||||
### Via Git (preferred)
|
||||
|
||||
1. Contact the maintainer to obtain push access, or fork on the Gitea instance.
|
||||
2. Create a branch: `git checkout -b feature/my-feature`.
|
||||
3. Commit with a clear subject line: `module: short description (≤ 72 chars)`.
|
||||
4. Push and open a pull request against `master`.
|
||||
|
||||
### Via patch
|
||||
|
||||
If you do not have push access:
|
||||
|
||||
```bash
|
||||
git format-patch origin/master
|
||||
```
|
||||
|
||||
Send the resulting `.patch` file(s) to
|
||||
`fatalerrors <at> geoffray-levasseur <dot> org`.
|
||||
|
||||
### Commit message format
|
||||
|
||||
```
|
||||
module: imperative short description
|
||||
|
||||
Optional longer explanation of what changed and why. Wrap at 72 characters.
|
||||
Reference issue numbers if applicable: closes #42.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 11. What will be rejected
|
||||
|
||||
- Code requiring packages not in a minimal Debian or CentOS install, unless optionnal.
|
||||
- Use of `eval`, `source`-based config loading, or other code-injection vectors.
|
||||
- Changes that break Bash 4.3 compatibility.
|
||||
- Patches without a passing `shellcheck` run.
|
||||
- New functions without `--help` support.
|
||||
|
||||
---
|
||||
|
||||
## 12. Financial contributions
|
||||
|
||||
Contact the maintainer by mail if you wish to make a financial contribution.
|
||||
+431
@@ -0,0 +1,431 @@
|
||||
# Frequently Asked Questions
|
||||
|
||||
---
|
||||
|
||||
## Installation & loading
|
||||
|
||||
**Q: How do I install profile automatically into my shell startup files?**
|
||||
|
||||
Run the installer directly (no need to source first):
|
||||
|
||||
```bash
|
||||
bash <installpath>/profile/profile.sh --install
|
||||
```
|
||||
|
||||
This appends the required `source` line to both `~/.bashrc` and `~/.profile`.
|
||||
To target only one file:
|
||||
|
||||
```bash
|
||||
bash <installpath>/profile/profile.sh --install --bashrc
|
||||
bash <installpath>/profile/profile.sh --install --profile
|
||||
```
|
||||
|
||||
The operation is idempotent — running it again will not add a duplicate line.
|
||||
|
||||
---
|
||||
|
||||
**Q: I ran `profile.sh` directly and got a warning about sourcing.**
|
||||
|
||||
profile.sh is designed to be *sourced*, not executed:
|
||||
|
||||
```bash
|
||||
source <installpath>/profile/profile.sh
|
||||
```
|
||||
|
||||
The only exception is `--install`, which must be passed to a direct execution
|
||||
(`bash profile.sh --install`) to set up the sourcing line automatically.
|
||||
|
||||
---
|
||||
|
||||
**Q: profile refuses to load and prints "This profile requires Bash 4.3 or higher."**
|
||||
|
||||
Your system's default shell is an older Bash (common on macOS, which ships
|
||||
Bash 3.x for licensing reasons). Install a newer Bash:
|
||||
|
||||
```bash
|
||||
# macOS
|
||||
brew install bash
|
||||
# then add /opt/homebrew/bin/bash to /etc/shells and chsh
|
||||
```
|
||||
|
||||
Or point your terminal emulator at the newer binary explicitly.
|
||||
|
||||
---
|
||||
|
||||
**Q: I sourced `profile.sh` but functions are not available in sub-shells or scripts.**
|
||||
|
||||
All public functions are exported with `export -f`. They are available in
|
||||
child Bash processes, but **not** in POSIX `sh` sub-shells. Make sure your
|
||||
scripts start with `#!/usr/bin/env bash`.
|
||||
|
||||
---
|
||||
|
||||
**Q: Can I use profile functions in scripts?**
|
||||
|
||||
Yes. The supported way is to source `profile.sh` from a Bash script:
|
||||
|
||||
```bash
|
||||
#!/usr/bin/env bash
|
||||
source /path/to/profile/profile.sh
|
||||
|
||||
taz -p auto -f lz mydir
|
||||
```
|
||||
|
||||
You can also source one module directly (for example
|
||||
`profile.d/compress.sh`) if you only need a subset of functions. The module
|
||||
loads the helpers it needs from the `helpers/` directory, which must stay next
|
||||
to `profile.d/`.
|
||||
|
||||
When you source a module directly, profile configuration parsing/loading from
|
||||
`profile.sh` is skipped, so defaults from `profile.conf` are not applied unless
|
||||
your script loads them explicitly.
|
||||
|
||||
`profile.sh` also detects whether the current shell is interactive. In
|
||||
non-interactive shells (typical script execution), interactive-only features
|
||||
are intentionally disabled: prompt setup, aliases, welcome/info messages, and
|
||||
startup update checks are not enabled.
|
||||
|
||||
In all cases, avoid aliases in scripts. Use real commands/functions instead,
|
||||
because alias expansion is interactive-shell oriented and can be disabled or
|
||||
behave differently in non-interactive execution.
|
||||
|
||||
---
|
||||
|
||||
**Q: I set `PROFILE_PATH` but profile still can't find its modules.**
|
||||
|
||||
`PROFILE_PATH` must be exported *before* you source `profile.sh`:
|
||||
|
||||
```bash
|
||||
export PROFILE_PATH=/opt/profile
|
||||
source /opt/profile/profile.sh
|
||||
```
|
||||
|
||||
If set after sourcing, `MYPATH` is already locked in and the variable has
|
||||
no effect.
|
||||
|
||||
---
|
||||
|
||||
**Q: Can I load profile system-wide via `/etc/profile`?**
|
||||
|
||||
It is not recommended. User `.bashrc` files frequently set variables that
|
||||
conflict with the aliases and locale functions defined here, leading to
|
||||
surprising behaviour. Per-user sourcing from `~/.bashrc` is the supported
|
||||
method.
|
||||
|
||||
---
|
||||
|
||||
## Configuration
|
||||
|
||||
**Q: I edited `profile.conf` but my changes have no effect.**
|
||||
|
||||
`profile.conf` is parsed once per shell session at load time. Open a new
|
||||
terminal (or `exec bash`) to pick up the changes. There is no live-reload.
|
||||
|
||||
---
|
||||
|
||||
**Q: How do I find out which configuration keys a module supports?**
|
||||
|
||||
Every supported key is documented with a comment in `profile.conf`.
|
||||
See also `README.md §4.2` for a consolidated table.
|
||||
|
||||
---
|
||||
|
||||
**Q: A key I set in `profile.conf` is being ignored.**
|
||||
|
||||
Check that:
|
||||
|
||||
1. The key is inside the correct `[section]` header.
|
||||
2. There is no leading space before the section name (`[section]` not
|
||||
`[ section ]`).
|
||||
3. The key is not commented out (no leading `#`).
|
||||
4. The value contains no backticks or `$(…)` — these are stripped by the
|
||||
parser as a security measure.
|
||||
|
||||
---
|
||||
|
||||
**Q: How do I force or auto-detect the correct `TERM` value?**
|
||||
|
||||
Set `TERM` in the `[system]` section of `profile.conf`:
|
||||
|
||||
```ini
|
||||
[system]
|
||||
# Auto-detect best available terminfo entry at startup (default)
|
||||
#TERM=smart
|
||||
|
||||
# Force a specific entry
|
||||
#TERM=xterm-256color
|
||||
```
|
||||
|
||||
When `TERM` is absent or set to `smart`, `term_set` probes your terminal
|
||||
emulator's environment variables (`COLORTERM`, `TERM_PROGRAM`, `VTE_VERSION`,
|
||||
`WT_SESSION`, `TMUX`, `STY`) and then tests terminfo entries in preference
|
||||
order (multiplexers first). If you are experiencing display issues, run
|
||||
`TERM=smart; term_set` interactively (`term_set` leaves an already-set `TERM`
|
||||
untouched) and check the result with `echo $TERM`. See *Prompt & theming* below for
|
||||
symptoms caused by a wrong `TERM` value.
|
||||
|
||||
---
|
||||
|
||||
## Prompt & theming
|
||||
|
||||
**Q: How do I change the prompt theme?**
|
||||
|
||||
Add to `profile.conf`:
|
||||
|
||||
```ini
|
||||
[prompt]
|
||||
PROMPT_THEME = dark
|
||||
```
|
||||
|
||||
Built-in names: `default`, `dark`, `light`, `solarized`, `solarized-light`,
|
||||
`monokai`, `monochrome`, `abyss`, `plasma`, `adwaita`, but you can create your
|
||||
own theme.
|
||||
|
||||
---
|
||||
|
||||
**Q: The solarized or solarized-light theme shows wrong colours.**
|
||||
|
||||
Those themes use 24-bit / true-colour ANSI sequences (`\e[38;2;R;G;Bm`).
|
||||
Test your terminal:
|
||||
|
||||
```bash
|
||||
printf '\e[38;2;38;139;210mTrue colour test\e[0m\n'
|
||||
```
|
||||
|
||||
If you see a solid blue word your terminal supports true colour.
|
||||
If you see garbage or plain text, switch to a 16-colour theme
|
||||
(`dark`, `default`, etc.) or upgrade your terminal emulator.
|
||||
|
||||
---
|
||||
|
||||
**Q: My prompt colours are missing, wrong, or ANSI escape codes appear as raw text.**
|
||||
|
||||
The most common cause is a `TERM` value that does not match what your terminal
|
||||
emulator actually supports — either it points to an entry that does not exist
|
||||
in the local `terminfo` database, or it under-describes the real capabilities
|
||||
(e.g. `TERM=xterm` when the emulator supports 256 colours).
|
||||
|
||||
Diagnose with:
|
||||
|
||||
```bash
|
||||
echo "TERM=$TERM"
|
||||
tput colors # should print 256 (or higher) for a capable terminal
|
||||
infocmp | head -5 # verify the loaded terminfo entry looks sane
|
||||
```
|
||||
|
||||
Common scenarios and fixes:
|
||||
|
||||
| Symptom | Likely cause | Fix |
|
||||
| --- | --- | --- |
|
||||
| `tput: unknown terminal` | `TERM` value has no terminfo entry | Remove the forced value and let `TERM=smart` auto-detect |
|
||||
| Only 8 colours instead of 256 | `TERM=xterm` instead of `xterm-256color` | Set `TERM=smart` or force `xterm-256color` |
|
||||
| Colours correct in plain shell but wrong inside tmux | tmux overwrites `TERM` | Set `TERM=smart` — `term_set` picks `tmux-256color` |
|
||||
| Colours correct outside screen but wrong inside it | Same, for GNU screen | Set `TERM=smart` — `term_set` picks `screen-256color` |
|
||||
| Prompt renders correctly but `rain` / `matrix` shows garbage | Terminal doesn't support the ANSI codes implied by `TERM` | Match `TERM` to the real emulator capability |
|
||||
|
||||
The recommended approach is to leave `TERM` unset (or set it to `smart`) in
|
||||
`profile.conf` and let `term_set` choose the best available entry automatically.
|
||||
Only force a specific value if auto-detection picks the wrong one.
|
||||
|
||||
Theme files are parsed, not executed. Only `PROMPT_COLOR_*` keys and the
|
||||
standard colour variable names from `disp.sh` (`Black`, `Blue`, `On_IBlack`,
|
||||
…) are accepted. Any other key — including custom variables — is rejected.
|
||||
See `README.md §4.4` for the full list of accepted keys and value forms.
|
||||
|
||||
---
|
||||
|
||||
**Q: Can a theme file contain shell logic or `$(…)` command substitutions?**
|
||||
|
||||
No, and intentionally so. Theme files are parsed line-by-line; shell
|
||||
constructs are never evaluated. This is a security boundary — a malicious
|
||||
theme file cannot execute code. Values must be a colour variable reference
|
||||
(`$Blue`) or a raw ANSI escape literal (`\e[0;34m`).
|
||||
|
||||
---
|
||||
|
||||
## Git helpers
|
||||
|
||||
**Q: What git helper functions does profile provide?**
|
||||
|
||||
All git helpers are defined in `profile.d/git.sh` (new in 4.1.0):
|
||||
|
||||
| Command | Purpose |
|
||||
| --- | --- |
|
||||
| `gst` | Compact status with branch tracking info |
|
||||
| `ggraph` | Decorated history graph |
|
||||
| `gsync` | Fetch and rebase onto upstream |
|
||||
| `gacp` | Add, commit and push in one command |
|
||||
| `greset` | Reset to upstream, stashing local changes first |
|
||||
| `gwip` | Quick WIP checkpoint commit |
|
||||
| `gprune` | Delete merged local branches |
|
||||
| `groot` | Print or cd to repository root |
|
||||
|
||||
All commands accept `-h` / `--help`.
|
||||
|
||||
---
|
||||
|
||||
**Q: Tab completion for `gacp` does not show modified files.**
|
||||
|
||||
Profile ships dedicated completions in `profile.d/bash-completion/git-completion.sh`,
|
||||
loaded automatically in interactive sessions. If completions are missing,
|
||||
check that the system git completion is installed:
|
||||
|
||||
```bash
|
||||
# Debian / Ubuntu
|
||||
apt-get install bash-completion
|
||||
# Fedora / RHEL
|
||||
dnf install bash-completion
|
||||
```
|
||||
|
||||
When the native git completion helpers are available, `gacp` path completion
|
||||
behaves exactly like `git add` (modified files, untracked files, directories).
|
||||
|
||||
---
|
||||
|
||||
**Q: `gacp` says "No files specified" even though I passed `-a`.**
|
||||
|
||||
The `-a` / `--auto` flag adds all modified files (equivalent to `git add -A`).
|
||||
The default depends on `GIT_GACP_AUTO_ADD` in `profile.conf`:
|
||||
|
||||
```ini
|
||||
[git]
|
||||
GIT_GACP_AUTO_ADD = 1
|
||||
```
|
||||
|
||||
Set to `1` to make `-a` the default.
|
||||
|
||||
---
|
||||
|
||||
## Functions
|
||||
|
||||
**Q: `meteo` prints "No city specified" even though I set a default.**
|
||||
|
||||
The key is `METEO_DEFAULT_CITY` (not `DEFAULT_CITY`), and it must be in the
|
||||
`[info]` section:
|
||||
|
||||
```ini
|
||||
[info]
|
||||
METEO_DEFAULT_CITY = Paris
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
**Q: `dwl` fails with "no download tool found".**
|
||||
|
||||
`dwl` requires one of `curl`, `wget`, or `fetch` to be installed.
|
||||
Install curl:
|
||||
|
||||
```bash
|
||||
# Debian / Ubuntu
|
||||
apt-get install curl
|
||||
|
||||
# Fedora / RHEL
|
||||
dnf install curl
|
||||
```
|
||||
|
||||
Or set `DWL_PREFERRED_TOOL` in `[net]` to whichever tool you have.
|
||||
|
||||
---
|
||||
|
||||
**Q: How do I limit how long `dwl` waits for a download?**
|
||||
|
||||
Use the `-t` / `--timeout` option:
|
||||
|
||||
```bash
|
||||
dwl -t 5 https://example.com/file.txt /tmp/file.txt
|
||||
```
|
||||
|
||||
This sets a 5-second cap on both the connection and the overall transfer.
|
||||
The timeout is propagated to `curl` (`--max-time` + `--connect-timeout`),
|
||||
`wget` (`--timeout`), or `fetch` (`-T`) transparently.
|
||||
|
||||
---
|
||||
|
||||
**Q: The prompt takes a long time to appear when my network is unavailable.**
|
||||
|
||||
Fixed in 4.1.0. `check_updates -q` (called at startup) now enforces a
|
||||
3-second network timeout. If the update server is unreachable the check
|
||||
fails silently and the prompt appears immediately.
|
||||
|
||||
---
|
||||
|
||||
**Q: `help` only shows a list of functions. Can I get usage for a specific one?**
|
||||
|
||||
Yes — pass the command name as an argument:
|
||||
|
||||
```bash
|
||||
help gacp
|
||||
help dwl
|
||||
help taz
|
||||
```
|
||||
|
||||
This calls `<command> --help` and prints the full usage for that function.
|
||||
|
||||
---
|
||||
|
||||
**Q: `pkgs` does not find packages I know are installed.**
|
||||
|
||||
`pkgs` uses `get_pkgmgr` to detect the active package manager and delegates
|
||||
to the appropriate tool. Supported families: `apt` (Debian/Ubuntu),
|
||||
`dnf` / `yum` (RHEL/Fedora), `zypper` (openSUSE), `pacman` (Arch),
|
||||
`apk` (Alpine), `portage` (Gentoo), `xbps` (Void), `nix`, `brew` (macOS).
|
||||
If your distribution is not detected, run `get_pkgmgr` to see what is
|
||||
identified, and check that the package manager binary is in your `PATH`.
|
||||
|
||||
---
|
||||
|
||||
**Q: `profile_upgrade` says "no update available" but I know there is one.**
|
||||
|
||||
`check_updates` compares the content of the remote `version` file against
|
||||
`$PROFVERSION`. If `UPDT_DEFAULT_BRANCH` in `[updates]` points to a different
|
||||
branch than your installation, the version files may not match. Check:
|
||||
|
||||
```bash
|
||||
cat "$MYPATH/version"
|
||||
```
|
||||
|
||||
and make sure `UPDT_DEFAULT_BRANCH` matches the branch you track.
|
||||
|
||||
---
|
||||
|
||||
## Compatibility
|
||||
|
||||
**Q: Some functions misbehave on macOS / Cygwin.**
|
||||
|
||||
Both environments ship non-GNU userland utilities with different flags and
|
||||
behaviour. profile is primarily developed and tested on Linux (Debian and
|
||||
RHEL families). macOS and Cygwin bugs are low priority; patches that add
|
||||
compatibility without breaking Linux support are welcome.
|
||||
|
||||
---
|
||||
|
||||
**Q: Can I use profile with ZSH?**
|
||||
|
||||
Not officially. Blockers include `local -A` (ZSH requires `typeset -A`) and
|
||||
`local -n` namerefs. A compatibility layer is listed in `doc/todo.md` but
|
||||
has not been implemented yet.
|
||||
|
||||
---
|
||||
|
||||
## Miscellaneous
|
||||
|
||||
**Q: How do I completely disable profile for one session?**
|
||||
|
||||
```bash
|
||||
PROFILE_DISABLED=1 bash --norc
|
||||
```
|
||||
|
||||
Or simply open a shell without sourcing `~/.bashrc` (`bash --norc`).
|
||||
|
||||
---
|
||||
|
||||
**Q: How do I report a bug?**
|
||||
|
||||
Open an issue on the
|
||||
[Gitea tracker](https://git.geoffray-levasseur.org/fatalerrors/profile/issues)
|
||||
or send a mail to `fatalerrors <at> geoffray-levasseur <dot> org` with:
|
||||
|
||||
- The exact command that triggered the bug
|
||||
- Your OS and Bash version (`bash --version`)
|
||||
- The module involved
|
||||
- Any relevant error output
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
Copyright 2021-2026 Geoffray Levasseur <fatalerrors@geoffray-levasseur.org>
|
||||
|
||||
This software is distributed under the BSD-3-Clause license with the
|
||||
following terms and conditions:
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification,
|
||||
are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation and/or
|
||||
other materials provided with the distribution.
|
||||
|
||||
3. Neither the name of the copyright holder nor the names of its contributors
|
||||
may be used to endorse or promote products derived from this software without
|
||||
specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
|
||||
IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
|
||||
INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
|
||||
BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
|
||||
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
|
||||
OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
|
||||
ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
@@ -0,0 +1,55 @@
|
||||
|
||||
# Known bugs
|
||||
|
||||
This document tracks currently known issues and limitations.
|
||||
|
||||
## Open issues
|
||||
|
||||
- None :-)
|
||||
|
||||
---
|
||||
|
||||
## Won't fix
|
||||
|
||||
These issues are caused by platform or environment limitations outside the scope of this
|
||||
project and will not be addressed in Bash.
|
||||
|
||||
### Prompt execution time is inaccurate in Windows Terminal (WSL)
|
||||
|
||||
- **Description:** In Windows Terminal the displayed duration includes idle and typing time,
|
||||
and is consistently higher than actual command execution time. In a native Linux terminal
|
||||
(including WSL shells inside Konsole, QTerminal, etc.) timing correctly starts on Enter and
|
||||
stops when the prompt reappears; in Windows Terminal, timer events appear tied to prompt
|
||||
display rather than to the Enter keypress.
|
||||
- **Cause:** Execution time is measured via a `DEBUG` trap and `PROMPT_COMMAND` using
|
||||
`$EPOCHREALTIME` (or `date +%s%N` on bash < 5) deltas. WSL + Windows Terminal introduces scheduling jitter between Bash signal
|
||||
events and the underlying Windows terminal layer that does not match wall-clock perception.
|
||||
- **Impact:** Cosmetic / observability only — commands execute normally.
|
||||
- **Status:** Not fixable in Bash; this is a limitation of the Windows Terminal / WSL
|
||||
integration layer.
|
||||
- **Workarounds:**
|
||||
- Use a native Linux terminal under WSL (Konsole, QTerminal, Terminator, etc.) to
|
||||
recover the expected Enter→prompt timing behavior.
|
||||
- Use `/usr/bin/time -p <command>` or the shell built-in `time` when accurate timing
|
||||
is required.
|
||||
- Treat prompt timing as an inacurate indicator in this environment.
|
||||
|
||||
### Rain/Matrix rendering is slow on Windows
|
||||
|
||||
- **Description:** The rain, matrix and rainbow terminal effects are significantly slower
|
||||
on Windows, especially with high density settings on every terminal software.
|
||||
- **Cause:** This is due to the way Windows handles terminal display updates, which is
|
||||
inherently less efficient than on Unix-like systems.
|
||||
- **Status:** Not fixable in Bash; this is a limitation of Windows terminal design.
|
||||
- **Workaround:** Lower the density parameter for better performance, or use a Unix-like
|
||||
environment for optimal speed.
|
||||
|
||||
### The prompt timer replaces an existing `DEBUG` trap
|
||||
|
||||
- **Description:** The execution time shown in the prompt is measured with a `DEBUG` trap
|
||||
installed when the profile is loaded. A `DEBUG` trap set earlier (bash-preexec and the
|
||||
tools built on it) is replaced.
|
||||
- **Cause:** bash hides the `DEBUG` trap from code run by `source`, so the profile cannot
|
||||
read it to chain to it.
|
||||
- **Workaround:** Load the profile before such tools. `PROMPT_COMMAND`, on the other hand,
|
||||
is preserved: `set_prompt` is added to it.
|
||||
@@ -0,0 +1,336 @@
|
||||
# profile.conf — example / reference configuration
|
||||
# Copy this file to the profile installation directory as "profile.conf"
|
||||
# and uncomment / edit the keys you want to change.
|
||||
#
|
||||
# Format:
|
||||
# [section_name] — starts a section
|
||||
# key = value — sets a key (whitespace around = is optional)
|
||||
# # comment — line comment
|
||||
#
|
||||
# All keys are optional. Sensible defaults apply when unset.
|
||||
# Values must not contain backticks or $(…) — those are stripped for security.
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
# ==============================================================================
|
||||
[system]
|
||||
# Bash history settings
|
||||
HISTSIZE=50000
|
||||
HISTFILESIZE=100000
|
||||
HISTIGNORE="&:[bf]g:exit"
|
||||
|
||||
# Default pager
|
||||
PAGER=less
|
||||
|
||||
# Terminal type.
|
||||
# smart — auto-detect the best available capability at startup (default)
|
||||
# <specific> — force a specific terminfo entry, e.g. xterm-256color or vt100
|
||||
#TERM=smart
|
||||
|
||||
# ==============================================================================
|
||||
[compress]
|
||||
# taz: Default archive format (-f/--format).
|
||||
# Supported: lz (default), xz, bz2, gz, lzo, tar, zip, zst
|
||||
#TAZ_DEFAULT_FORMAT=lz
|
||||
|
||||
# taz: Number of compression threads.
|
||||
# auto — detect CPU count at runtime (default)
|
||||
# N — explicit positive integer
|
||||
#TAZ_DEFAULT_THREADS=auto
|
||||
|
||||
# taz: Compression level 1 (fast/large) … 9 (slow/small).
|
||||
#TAZ_DEFAULT_LEVEL=6
|
||||
|
||||
# utaz: Delete source archive after successful extraction (0=no, 1=yes).
|
||||
#UTAZ_DEFAULT_DELETE=0
|
||||
|
||||
# utaz: Subdirectory creation policy.
|
||||
# auto — create one only when the archive has multiple top-level entries
|
||||
# always — always extract into a new subdirectory
|
||||
# never — always flatten into the current directory
|
||||
#UTAZ_DEFAULT_DIR_MODE=auto
|
||||
|
||||
# utaz: Match archive extensions case-insensitively, so that FOO.ZIP or
|
||||
# bar.Tar.GZ are recognised (0=no, 1=yes). Overridden per call with
|
||||
# -i/--ignore-case or -s/--case-sensitive.
|
||||
#UTAZ_DEFAULT_IGNORE_CASE=0
|
||||
|
||||
# ==============================================================================
|
||||
[debug]
|
||||
# Activate the error trace (settrace --on) at startup of interactive shells:
|
||||
# every failing command then prints its exit status and a call stack to stderr.
|
||||
# Verbose; meant for debugging scripts and the profile itself (0=no, 1=yes).
|
||||
# Can be toggled at any time with settrace --on / --off.
|
||||
#SETTRACE_DEFAULT_ENABLED=0
|
||||
|
||||
# ==============================================================================
|
||||
[disp]
|
||||
# Uncomment to disable ANSI colours in profile's own output messages. For the
|
||||
# prompt use a non-colored theme in [prompt] section of that file.
|
||||
#NO_COLOR=1
|
||||
|
||||
# ==============================================================================
|
||||
[filefct]
|
||||
# expandlist: Separator between items (default: space). Use \n for newline.
|
||||
#EXPANDLIST_DEFAULT_SEPARATOR=" "
|
||||
|
||||
# clean: Recurse into subdirectories by default (0=no, 1=yes).
|
||||
#CLEAN_DEFAULT_RECURSIVE=0
|
||||
|
||||
# rmspc: Replacement character for spaces in filenames (default: underscore).
|
||||
#RMSPC_DEFAULT_CHAR=_
|
||||
|
||||
# findbig: Number of results to display (default: 10).
|
||||
#FINDBIG_DEFAULT_LIMIT=10
|
||||
|
||||
# ==============================================================================
|
||||
[fun]
|
||||
# busy: Hex pattern searched in /dev/urandom hexdump.
|
||||
#BUSY_DEFAULT_PATTERN=ca fe
|
||||
|
||||
# busy: Delay between matched lines in milliseconds (0 = no delay).
|
||||
#BUSY_DEFAULT_DELAY=0
|
||||
|
||||
# fake_compile: Minimum delay between output lines in milliseconds.
|
||||
#FAKE_COMPILE_DEFAULT_MIN_DELAY=40
|
||||
|
||||
# fake_compile: Maximum delay between output lines in milliseconds.
|
||||
#FAKE_COMPILE_DEFAULT_MAX_DELAY=150
|
||||
|
||||
# fake_compile: Default language preset (c, cpp, java, python, random).
|
||||
#FAKE_COMPILE_DEFAULT_LANG=c
|
||||
|
||||
# hack: Minimum delay between output lines in milliseconds.
|
||||
#HACK_DEFAULT_MIN_DELAY=60
|
||||
|
||||
# hack: Maximum delay between output lines in milliseconds.
|
||||
#HACK_DEFAULT_MAX_DELAY=250
|
||||
|
||||
# ==============================================================================
|
||||
[git]
|
||||
# Fallback main branch name used when remote HEAD cannot be detected.
|
||||
#GIT_MAIN_BRANCH=main
|
||||
|
||||
# Default remote used by git helper functions.
|
||||
#GIT_DEFAULT_REMOTE=origin
|
||||
|
||||
# Prefix used by gwip when generating automatic checkpoint messages.
|
||||
#GIT_WIP_PREFIX=wip
|
||||
|
||||
# gacp: Automatically add all modified files (git add -A) when no explicit file
|
||||
# list is provided. Set to 0 to require explicit file paths or the -a flag.
|
||||
#GIT_GACP_AUTO_ADD=1
|
||||
|
||||
# ==============================================================================
|
||||
[info]
|
||||
# meteo: Default city when no argument is given. Leave unset to require an
|
||||
# explicit city argument every time.
|
||||
#METEO_DEFAULT_CITY=Paris
|
||||
|
||||
# ==============================================================================
|
||||
[lang]
|
||||
# Comma-separated alias:locale pairs. One function is generated per entry.
|
||||
# Example: SET_LOCALE="fr:fr_FR.UTF-8,us:en_US.UTF-8,es:es_ES.UTF-8"
|
||||
# creates setfr, setus, setes.
|
||||
#SET_LOCALE=fr:fr_FR.UTF-8,us:en_US.UTF-8
|
||||
|
||||
# Alias to activate at login. Must match one of the aliases defined in SET_LOCALE above.
|
||||
# Example: DEFAULT_LANG=fr → calls setfr at startup.
|
||||
# Leave unset to keep the system default locale.
|
||||
#DEFAULT_LANG=fr
|
||||
|
||||
# ==============================================================================
|
||||
[net]
|
||||
# dwl: Force a specific download tool (curl, wget, fetch).
|
||||
# Unset = auto-detect (curl preferred, then wget, then fetch).
|
||||
#DWL_PREFERRED_TOOL=curl
|
||||
|
||||
# dwl: Enable resume mode by default (supports curl/wget file downloads).
|
||||
# Values accepted: 1/0, true/false, yes/no, on/off.
|
||||
#DWL_DEFAULT_RESUME=0
|
||||
|
||||
# myextip: HTTPS JSON API used for the external IP lookup (default below).
|
||||
# Also supported: https://ipwho.is/ (more detailed: ISP and AS kept apart,
|
||||
# full country name). ip-api.com is not recommended: its free tier is HTTP only
|
||||
# and https://ip-api.com/ answers 403.
|
||||
#MYEXTIP_DEFAULT_URL=https://ipinfo.io/json
|
||||
|
||||
# ==============================================================================
|
||||
[packages]
|
||||
# pkgs: Case-insensitive search by default (0=no, 1=yes).
|
||||
#PKGS_DEFAULT_IGNORE_CASE=0
|
||||
|
||||
# ==============================================================================
|
||||
[processes]
|
||||
# ppu: ps output columns (comma-separated ps field names).
|
||||
#PPU_DEFAULT_FORMAT=pid,user,%cpu,%mem,start,time,command
|
||||
|
||||
# ku: Signal sent when killing a user's processes (without SIG prefix).
|
||||
#KU_DEFAULT_SIGNAL=TERM
|
||||
|
||||
# ==============================================================================
|
||||
[prompt]
|
||||
# Name of the prompt theme to load, or an explicit path to a .theme file.
|
||||
# Built-in themes: default, dark, light, solarized, solarized-light,
|
||||
# monokai, monochrome, abyss, plasma, adwaita
|
||||
# Solarized variants require a terminal with 24-bit true-colour support.
|
||||
#PROMPT_THEME=default
|
||||
|
||||
# Directory searched for bare theme names. Defaults to profile.d/themes/.
|
||||
#PROMPT_THEME_DIR=/path/to/my/themes
|
||||
|
||||
# Individual colour overrides — these always win over the loaded theme.
|
||||
# Values must be colour variable names from disp.sh (e.g. $Blue, $On_IBlack)
|
||||
# or raw ANSI escape sequences (e.g. \e[0;34m).
|
||||
#
|
||||
# Clock segment
|
||||
#PROMPT_COLOR_TIME_FG=$Blue
|
||||
#PROMPT_COLOR_TIME_BG=$On_IBlack
|
||||
#
|
||||
# Main bar background
|
||||
#PROMPT_COLOR_BAR_BG=$On_Blue
|
||||
#
|
||||
# Exit-code segment — success
|
||||
#PROMPT_COLOR_OK_FG=$White
|
||||
#PROMPT_COLOR_OK_MARK=$Green
|
||||
#
|
||||
# Exit-code segment — failure
|
||||
#PROMPT_COLOR_ERR_BG=$On_Red
|
||||
#PROMPT_COLOR_ERR_FG=$White
|
||||
#PROMPT_COLOR_ERR_MARK=$BYellow
|
||||
#
|
||||
# User / host
|
||||
#PROMPT_COLOR_ROOT_FG=$Red
|
||||
#PROMPT_COLOR_USER_FG=$Green
|
||||
#
|
||||
# Working directory
|
||||
#PROMPT_COLOR_DIR_FG=$ICyan
|
||||
#
|
||||
# Context segment (Git branch / Conda environment) on the top bar
|
||||
#PROMPT_COLOR_CTX_FG=$BIYellow
|
||||
#
|
||||
# Show Git branch at the end of the top bar when inside a repository.
|
||||
#PROMPT_SHOW_GIT=1
|
||||
#
|
||||
# Include Git dirty marker and upstream drift (+ahead/-behind) in the context.
|
||||
#PROMPT_SHOW_GIT_STATUS=1
|
||||
#
|
||||
# Timeout in seconds for git diff and git rev-list operations.
|
||||
# If exceeded, the prompt displays git:<branch>? instead of full status.
|
||||
#PROMPT_GIT_TIMEOUT=2
|
||||
#
|
||||
# Show Conda environment name at the end of the top bar when active.
|
||||
#PROMPT_SHOW_CONDA=1
|
||||
#
|
||||
# Show Python venv name when active (ignored if Conda is active).
|
||||
#PROMPT_SHOW_VENV=1
|
||||
#
|
||||
# Show session markers (ssh, tmux, screen) when applicable.
|
||||
#PROMPT_SHOW_SESSION=1
|
||||
|
||||
# ==============================================================================
|
||||
[pwd]
|
||||
# genpwd: Default password length.
|
||||
#GENPWD_DEFAULT_LENGTH=16
|
||||
|
||||
# genpwd: Maximum occurrences of any single character.
|
||||
#GENPWD_DEFAULT_OCCURS=2
|
||||
|
||||
# genpwd: Number of passwords generated per invocation.
|
||||
#GENPWD_DEFAULT_COUNT=1
|
||||
|
||||
# genpwd: Include symbols (1=yes, 0=no).
|
||||
#GENPWD_DEFAULT_SYMBOLS=1
|
||||
|
||||
# genpwd: Include uppercase letters (1=yes, 0=no).
|
||||
#GENPWD_DEFAULT_UPPERCASE=1
|
||||
|
||||
# genpwd: Include lowercase letters (1=yes, 0=no).
|
||||
#GENPWD_DEFAULT_LOWERCASE=1
|
||||
|
||||
# genpwd: Include digits (1=yes, 0=no).
|
||||
#GENPWD_DEFAULT_NUMBERS=1
|
||||
|
||||
# pwdscore: Show verbose breakdown by default (1=yes, 0=no).
|
||||
#PWDSCORE_DEFAULT_VERBOSE=0
|
||||
|
||||
# ==============================================================================
|
||||
[rain]
|
||||
# rain: Falling speed — integer/100 gives seconds (5 → 0.05 s).
|
||||
# Values < 1 are used as raw seconds.
|
||||
#RAIN_DEFAULT_SPEED=5
|
||||
|
||||
# rain: Colour theme. Supported: white (default), green, blue, red, yellow, cyan
|
||||
#RAIN_DEFAULT_COLOR=white
|
||||
|
||||
# rain: Maximum number of simultaneous falling elements.
|
||||
# Leave unset to keep the terminal-size-based dynamic default.
|
||||
#RAIN_DEFAULT_DENSITY=80
|
||||
|
||||
# rainbow: Horizontal color shift speed — integer/100 gives seconds (4 → 0.04 s).
|
||||
# Values < 1 are used as raw seconds.
|
||||
#RAINBOW_DEFAULT_SPEED=4
|
||||
|
||||
# matrix: Falling speed.
|
||||
#MATRIX_DEFAULT_SPEED=3.5
|
||||
|
||||
# matrix: Colour theme. Supported: green (default), blue, red, yellow, cyan, white
|
||||
#MATRIX_DEFAULT_COLOR=green
|
||||
|
||||
# matrix: Maximum number of simultaneous falling elements.
|
||||
# Leave unset to keep the terminal-size-based dynamic default.
|
||||
#MATRIX_DEFAULT_DENSITY=120
|
||||
|
||||
# matrix: Character set. Supported: binary (default), kana, ascii
|
||||
#MATRIX_DEFAULT_CHARSET=binary
|
||||
|
||||
# ==============================================================================
|
||||
[ssh]
|
||||
# ssr: Default SSH options prepended to every ssr call.
|
||||
# Examples:
|
||||
# SSH_DEFAULT_OPT=-Y # X11 forwarding
|
||||
# SSH_DEFAULT_OPT=-Y -o StrictHostKeyChecking=accept-new
|
||||
# SSH_DEFAULT_OPT= # no default options
|
||||
#SSH_DEFAULT_OPT=-Y
|
||||
|
||||
# ==============================================================================
|
||||
[updates]
|
||||
# Git branch used for update checks and profile_upgrade.
|
||||
# Changing this causes profile_upgrade to automatically switch the local
|
||||
# checkout to the new branch on the next run and display a warning.
|
||||
#UPDT_DEFAULT_BRANCH=master
|
||||
|
||||
# ==============================================================================
|
||||
[general]
|
||||
# General-purpose section — set any environment variable you need globally.
|
||||
# Good place for compilation flags, personal PATH additions, etc.
|
||||
#CFLAGS=-O2 -pipe -march=native
|
||||
#CXXFLAGS=$CFLAGS
|
||||
#MAKEFLAGS=-j4
|
||||
#PKGSOURCES=/usr/local/src
|
||||
|
||||
# ==============================================================================
|
||||
[aliases]
|
||||
# Command aliases loaded for interactive shells only.
|
||||
# The value is the full command string; the key becomes the alias name.
|
||||
# The base command must be executable; if not, the alias is silently skipped.
|
||||
#
|
||||
# ls
|
||||
ll=ls -laFh --color=auto
|
||||
la=ls -Ah --color=auto
|
||||
l=ls -CF --color=auto
|
||||
ls=ls --color=auto
|
||||
#
|
||||
# grep
|
||||
grep=grep --color=auto
|
||||
egrep=egrep --color=auto
|
||||
fgrep=fgrep --color=auto
|
||||
#
|
||||
# disk usage
|
||||
df=df -H
|
||||
du=du -ch
|
||||
#
|
||||
# make shortcuts
|
||||
#mk=make
|
||||
#mkin=make install
|
||||
|
||||
# End of profile.conf.example
|
||||
@@ -0,0 +1,296 @@
|
||||
[system]
|
||||
# System section is used to set Bash behavior and other system related
|
||||
# variables, such as the default pager, the terminal type, etc.
|
||||
# Set bash history
|
||||
HISTSIZE=50000
|
||||
HISTIGNORE="&:[bf]g:exit"
|
||||
|
||||
# Set default pager
|
||||
PAGER=less
|
||||
|
||||
# Set terminal colors behavior
|
||||
TERM=xterm-256color
|
||||
|
||||
[compress]
|
||||
# Section used by compress.sh (taz and utaz functions).
|
||||
|
||||
# taz: Default archive format when -f/--format is not specified.
|
||||
# Supported values: lz (default), xz, bz2, gz, lzo, tar
|
||||
#TAZ_DEFAULT_FORMAT=lz
|
||||
|
||||
# taz: Default number of compression threads when -p/--parallel is not specified.
|
||||
# Set to the number of CPU cores to use parallel compression where supported.
|
||||
#TAZ_DEFAULT_THREADS=1
|
||||
|
||||
# taz: Default compression level when none is given (1=fast/big .. 9=slow/small).
|
||||
#TAZ_DEFAULT_LEVEL=6
|
||||
|
||||
# utaz: Delete source archives after successful extraction (0=no, 1=yes).
|
||||
#UTAZ_DEFAULT_DELETE=0
|
||||
|
||||
# utaz: Default directory creation mode when neither --create-dir nor --no-dir is given.
|
||||
# Supported values:
|
||||
# auto (default) — create a subdirectory only when the archive contains
|
||||
# multiple top-level entries or a bare file
|
||||
# always — always extract into a new subdirectory
|
||||
# never — always flatten extraction into the current directory
|
||||
#UTAZ_DEFAULT_DIR_MODE=auto
|
||||
|
||||
# utaz: Match archive extensions case-insensitively, so that FOO.ZIP or
|
||||
# bar.Tar.GZ are recognised (0=no, 1=yes). Overridden per call with
|
||||
# -i/--ignore-case or -s/--case-sensitive.
|
||||
#UTAZ_DEFAULT_IGNORE_CASE=0
|
||||
|
||||
[debug]
|
||||
# Section used by debug.sh
|
||||
|
||||
# Activate the error trace (settrace --on) at startup of interactive shells:
|
||||
# every failing command then prints its exit status and a call stack to stderr
|
||||
# (0=no, 1=yes).
|
||||
#SETTRACE_DEFAULT_ENABLED=0
|
||||
|
||||
[disp]
|
||||
# Section used by disp.sh
|
||||
|
||||
# Set to any value to disable colors in internal profile output (not controling binary output)
|
||||
# NO_COLOR=1
|
||||
|
||||
[filefct]
|
||||
# Section used by filefct.sh
|
||||
|
||||
# expandlist: Default output separator between items (default: space).
|
||||
# Use \n for newline, or any other character/string.
|
||||
#EXPANDLIST_DEFAULT_SEPARATOR=" "
|
||||
|
||||
# clean: Enable recursive mode by default (0=no, 1=yes).
|
||||
#CLEAN_DEFAULT_RECURSIVE=0
|
||||
|
||||
# rmspc: Default character used to replace spaces in filenames (default: underscore).
|
||||
# Set to empty to concatenate words without separator (equivalent to --subst-char with no value).
|
||||
#RMSPC_DEFAULT_CHAR=_
|
||||
|
||||
# findbig: Default number of results to return (default: 10).
|
||||
#FINDBIG_DEFAULT_LIMIT=10
|
||||
|
||||
[fun]
|
||||
# Section used by fun.sh (busy function).
|
||||
|
||||
# busy: Default hex pattern to search for in /dev/urandom hexdump (default: "ca fe").
|
||||
#BUSY_DEFAULT_PATTERN=ca fe
|
||||
|
||||
# busy: Default delay between matched lines in milliseconds (default: 0 = no delay).
|
||||
#BUSY_DEFAULT_DELAY=0
|
||||
|
||||
[info]
|
||||
# Section used by info.sh
|
||||
|
||||
# meteo: Default city used when no city argument is given.
|
||||
# Unset or empty disables the fallback and requires an explicit city argument.
|
||||
METEO_DEFAULT_CITY="Toulouse"
|
||||
|
||||
[lang]
|
||||
# Section used by lang.sh
|
||||
# List of locale shortcuts to build, in the form "shortcut:locale,...".
|
||||
# Generate a function setXX for each shortcut defined.
|
||||
SET_LOCALE="fr:fr_FR.UTF-8,us:en_US.UTF-8"
|
||||
|
||||
[net]
|
||||
# Section used by net.sh (dwl, myextip functions).
|
||||
|
||||
# dwl: Force a specific download tool instead of auto-detecting (curl → wget → fetch).
|
||||
# Supported values: curl, wget, fetch. Unset uses auto-detection (default).
|
||||
#DWL_PREFERRED_TOOL=curl
|
||||
|
||||
# dwl: Enable resume mode by default (supports curl/wget file downloads).
|
||||
# Accepted values: 1/0, true/false, yes/no, on/off.
|
||||
#DWL_DEFAULT_RESUME=0
|
||||
|
||||
# myextip: API endpoint URL used to retrieve external IP information.
|
||||
# Default: https://ipinfo.io/json
|
||||
# Compatible alternative: https://ipwho.is/ (ip-api.com is HTTP only on its free tier).
|
||||
#MYEXTIP_DEFAULT_URL=https://ipinfo.io/json
|
||||
|
||||
[packages]
|
||||
# Section used by packages.sh
|
||||
|
||||
# pkgs: Enable case-insensitive search by default (1=yes, 0=no; default: 0).
|
||||
#PKGS_DEFAULT_IGNORE_CASE=0
|
||||
|
||||
[processes]
|
||||
# Section used by processes.sh
|
||||
|
||||
# ppu: Output columns passed to ps -o when listing processes for a user.
|
||||
# Comma-separated list of ps field names. Default: pid,user,%cpu,%mem,start,time,command
|
||||
#PPU_DEFAULT_FORMAT=pid,user,%cpu,%mem,start,time,command
|
||||
|
||||
# ku: Signal sent to processes when killing a user's session (default: TERM).
|
||||
# Use signal names without the SIG prefix (e.g. TERM, KILL, HUP).
|
||||
#KU_DEFAULT_SIGNAL=TERM
|
||||
|
||||
[prompt]
|
||||
# Section used by prompt.sh
|
||||
|
||||
# Name of the theme to load, or an explicit path to a .theme file.
|
||||
# Bare names are resolved as $PROMPT_THEME_DIR/<name>.theme.
|
||||
# When unset, no theme is loaded and the hardcoded fallback colours are used.
|
||||
#PROMPT_THEME=default
|
||||
|
||||
# Directory that contains .theme files. Defaults to profile.d/themes/ inside
|
||||
# the profile installation directory.
|
||||
#PROMPT_THEME_DIR=/path/to/themes
|
||||
|
||||
# Individual colour overrides. These always win over the loaded theme.
|
||||
# Values must be valid ANSI escape sequences as exported by disp.sh, e.g.:
|
||||
# \e[0;34m (Blue) \e[42m (On_Green) \e[1;32m (BGreen)
|
||||
# All keys listed below correspond to variables exported by disp.sh.
|
||||
|
||||
# Clock segment
|
||||
#PROMPT_COLOR_TIME_FG=$Blue
|
||||
#PROMPT_COLOR_TIME_BG=$On_IBlack
|
||||
|
||||
# Main bar background (success and info)
|
||||
#PROMPT_COLOR_BAR_BG=$On_Blue
|
||||
|
||||
# Exit-code segment — success state
|
||||
#PROMPT_COLOR_OK_FG=$White
|
||||
#PROMPT_COLOR_OK_MARK=$Green
|
||||
|
||||
# Exit-code segment — failure state
|
||||
#PROMPT_COLOR_ERR_BG=$On_Red
|
||||
#PROMPT_COLOR_ERR_FG=$White
|
||||
#PROMPT_COLOR_ERR_MARK=$BYellow
|
||||
|
||||
# User/host colours
|
||||
#PROMPT_COLOR_ROOT_FG=$Red
|
||||
#PROMPT_COLOR_USER_FG=$Green
|
||||
|
||||
# Working directory
|
||||
#PROMPT_COLOR_DIR_FG=$ICyan
|
||||
|
||||
[pwd]
|
||||
# Section used by pwd.sh (genpwd and pwdscore functions).
|
||||
|
||||
# genpwd: Default password length (default: 16).
|
||||
#GENPWD_DEFAULT_LENGTH=16
|
||||
|
||||
# genpwd: Maximum occurrences of any single character (default: 2).
|
||||
#GENPWD_DEFAULT_OCCURS=2
|
||||
|
||||
# genpwd: Number of passwords generated when no count argument is given (default: 1).
|
||||
#GENPWD_DEFAULT_COUNT=1
|
||||
|
||||
# genpwd: Include symbols in the character pool (1=yes, 0=no; default: 1).
|
||||
#GENPWD_DEFAULT_SYMBOLS=1
|
||||
|
||||
# genpwd: Include uppercase letters in the character pool (1=yes, 0=no; default: 1).
|
||||
#GENPWD_DEFAULT_UPPERCASE=1
|
||||
|
||||
# genpwd: Include lowercase letters in the character pool (1=yes, 0=no; default: 1).
|
||||
#GENPWD_DEFAULT_LOWERCASE=1
|
||||
|
||||
# genpwd: Include digits in the character pool (1=yes, 0=no; default: 1).
|
||||
#GENPWD_DEFAULT_NUMBERS=1
|
||||
|
||||
# pwdscore: Show verbose breakdown by default (1=yes, 0=no; default: 0).
|
||||
#PWDSCORE_DEFAULT_VERBOSE=0
|
||||
|
||||
[rain]
|
||||
# Section used by rain.sh (rain and matrix functions).
|
||||
|
||||
# rain: Default speed value, using the /100 scale (5 => 0.050s, 10 => 0.100s).
|
||||
# Values < 1 are interpreted as raw seconds.
|
||||
#RAIN_DEFAULT_SPEED=5
|
||||
|
||||
# rain: Default color theme.
|
||||
# Supported values: white (default), green, blue, red, yellow, cyan
|
||||
#RAIN_DEFAULT_COLOR=white
|
||||
|
||||
# rain: Maximum number of simultaneous falling elements.
|
||||
# Leave unset to keep the terminal-size-based dynamic default.
|
||||
#RAIN_DEFAULT_DENSITY=80
|
||||
|
||||
# matrix: Default speed value, using the /100 scale (3.5 => 0.035s).
|
||||
#MATRIX_DEFAULT_SPEED=3.5
|
||||
|
||||
# matrix: Default color theme.
|
||||
# Supported values: green (default), blue, red, yellow, cyan, white
|
||||
#MATRIX_DEFAULT_COLOR=green
|
||||
|
||||
# matrix: Maximum number of simultaneous falling elements.
|
||||
# Leave unset to keep the terminal-size-based dynamic default.
|
||||
#MATRIX_DEFAULT_DENSITY=120
|
||||
|
||||
# matrix: Default character set.
|
||||
# Supported values: binary (default), kana, ascii
|
||||
MATRIX_DEFAULT_CHARSET=kana
|
||||
|
||||
[ssh]
|
||||
# Section used by ssh.sh
|
||||
|
||||
# ssr: Default SSH options prepended to every ssr invocation.
|
||||
# Options are word-split, so space-separated flags are supported.
|
||||
# The default behaviour without this key is equivalent to: SSH_DEFAULT_OPT=-Y
|
||||
# Set to an empty value to pass no default options.
|
||||
# Examples:
|
||||
# SSH_DEFAULT_OPT=-Y # X11 forwarding (original default)
|
||||
# SSH_DEFAULT_OPT=-Y -o StrictHostKeyChecking=accept-new
|
||||
# SSH_DEFAULT_OPT= # no default options
|
||||
#SSH_DEFAULT_OPT=-Y
|
||||
|
||||
[updates]
|
||||
# Section used by updates.sh
|
||||
#
|
||||
# UPDT_DEFAULT_BRANCH — Git branch used for update checks and upgrades.
|
||||
# Defaults to 'master' when unset. Changing this value will cause
|
||||
# profile_upgrade to automatically switch the local checkout to the new
|
||||
# branch on the next upgrade and display a warning.
|
||||
#UPDT_DEFAULT_BRANCH=master
|
||||
|
||||
[general]
|
||||
# General section allow to set any variable that can be used by the user.
|
||||
# It is also a good place to set freely global variables for personal use.
|
||||
# Set some compiling values
|
||||
CFLAGS="-O2 -pipe -march=native"
|
||||
CXXFLAGS="$CFLAGS"
|
||||
MAKEFLAGS='-j12'
|
||||
PKGSOURCES='/share/src/archives'
|
||||
|
||||
[aliases]
|
||||
# Aliases section is used to set user aliases, it is loaded only for
|
||||
# interactive shells.
|
||||
# Various ls aliases
|
||||
ll='ls -laFh --color=auto'
|
||||
la='ls -Ah --color=auto'
|
||||
l='ls -CF --color=auto'
|
||||
ls='ls --color=auto'
|
||||
|
||||
# Add color to grep output
|
||||
grep='grep --color=auto'
|
||||
egrep='egrep --color=auto'
|
||||
fgrep='fgrep --color=auto'
|
||||
|
||||
# Quick find alias
|
||||
qfind="find . -name "
|
||||
|
||||
# Some alias for compiling
|
||||
mk='make'
|
||||
mkck='make check'
|
||||
mkin='make install'
|
||||
mkdin='make DESTDIR=$PWD/dest-install install'
|
||||
|
||||
# ssh alias with X11 forwarding, without right restriction
|
||||
ssh='ssh -Y'
|
||||
|
||||
# Resume mode for wget
|
||||
wget='wget -c'
|
||||
|
||||
# Human readable by default
|
||||
df='df -H'
|
||||
du='du -ch'
|
||||
sdu='du -sk ./* | sort -n'
|
||||
hdu='du -hs ./* | sort -H'
|
||||
|
||||
# Readable dmesg timestamps
|
||||
dmesg='dmesg -T'
|
||||
|
||||
# End of profile.conf
|
||||
+174
@@ -0,0 +1,174 @@
|
||||
# profile — to-do list
|
||||
|
||||
Items marked **[easy]**, **[medium]**, or **[hard]** give a rough effort hint.
|
||||
Items marked **[breaking]** would change existing behaviour and require a
|
||||
version-bump.
|
||||
|
||||
---
|
||||
|
||||
## Shell / compatibility
|
||||
|
||||
- [ ] **ZSH compatibility layer** — most functions work already; the remaining
|
||||
blockers are `local -A` (no associative arrays in ZSH without `typeset -A`)
|
||||
and `local -n` namerefs. A thin compatibility shim would open the project to
|
||||
ZSH users. **[hard]**
|
||||
- [ ] **Bash completion** — add a more bash completion directory and write
|
||||
`_profile_upgrade`, `_taz`, `_utaz`, `_meteo`, etc. completions so that
|
||||
`<Tab>` works on all public functions. **[medium]**
|
||||
|
||||
---
|
||||
|
||||
## Prompt & theming
|
||||
|
||||
- [x] **Git branch in prompt** — show the current branch name (with dirty and
|
||||
upstream drift indicators) in the PS1 bar when inside a Git repository,
|
||||
gated by `[prompt]` config keys. **[medium]**
|
||||
- [x] **Virtual-env / conda indicator** — detect `$VIRTUAL_ENV` / `$CONDA_DEFAULT_ENV`
|
||||
and display the active environment in the prompt bar. **[easy]**
|
||||
- [x] **Session context markers** — display lightweight session markers
|
||||
(`ssh`, `tmux`, `screen`) at the end of the prompt bar, gated by
|
||||
`[prompt]` config keys. **[easy]**
|
||||
- [x] **True-colour terminal auto-detection** — query `$COLORTERM` and
|
||||
`$TERM` at load time; automatically fall back from a 24-bit theme to its
|
||||
16-colour equivalent when the terminal does not support true colour. **[medium]**
|
||||
- [ ] **True-colour variants of other themes** — create `monokai-tc.theme`,
|
||||
`abyss-tc.theme`, etc. using the same `\e[38;2;R;G;Bm` approach as the
|
||||
Solarized themes. **[easy]** _(per theme)_
|
||||
- [X] **Theme preview command** — add a `theme_preview` (or `profile_theme`)
|
||||
function that renders a colour swatch and a sample prompt line for the
|
||||
currently loaded theme, so users can evaluate themes without reloading
|
||||
the session. **[medium]**
|
||||
- [ ] **`load_theme` — DEFAULTCOL rebuild** — after overriding `DEFAULTFG` and
|
||||
`DEFAULTBG`, automatically recompute `DEFAULTCOL` from the new values instead
|
||||
of requiring the theme file to set it explicitly. **[easy]**
|
||||
|
||||
---
|
||||
|
||||
## Module improvements
|
||||
|
||||
### compress
|
||||
|
||||
- [ ] **`taz` progress bar** — show a `pv` / `dd`-based progress indicator when
|
||||
compressing large trees, gated behind a `-p` flag. **[medium]**
|
||||
- [ ] **`utaz` integrity check** — run `tar -tOf` / `unzip -t` / `7z t` before
|
||||
extracting and abort if the archive is corrupt. **[easy]**
|
||||
|
||||
### filefct
|
||||
|
||||
- [x] **`findbig` / `findzero` / `finddead` — `fd` integration** — optionally
|
||||
use `fd` instead of `find` when available for faster traversal. **[easy]**
|
||||
- [x] **`file_stats` — human-readable totals** — add `--human` flag to emit
|
||||
sizes in K/M/G instead of bytes. **[easy]**
|
||||
|
||||
### info
|
||||
|
||||
- [X] **`showinfo` fallback** — when neither `neofetch` nor `fastfetch` is
|
||||
installed, print a minimal sysinfo block (hostname, OS, kernel, uptime,
|
||||
CPU, RAM) using pure Bash + `/proc`. **[medium]**
|
||||
|
||||
### net
|
||||
|
||||
- [X] **`dwl` resume support** — pass `-C -` to curl / `--continue-at -` to
|
||||
wget for interrupted downloads; gate behind a `-r` flag. **[easy]**
|
||||
- [ ] **`myextip` multiple providers** — fall back to a secondary URL
|
||||
(configurable via `MYEXTIP_FALLBACK_URL`) when the primary times out.
|
||||
**[easy]**
|
||||
|
||||
### processes
|
||||
|
||||
- [X] **`ku` dry-run flag** — add `-n` / `--dry-run` to print what would be
|
||||
killed without acting. **[easy]**
|
||||
|
||||
### pwd
|
||||
|
||||
- [ ] **`genpwd` passphrase mode** — add `-w` / `--words N` to generate
|
||||
word-based passphrases (diceware-style) from `/usr/share/dict/words`.
|
||||
**[medium]**
|
||||
|
||||
### ssh
|
||||
|
||||
- [ ] **SSH agent management** — add `ssh_agent_start` / `ssh_agent_stop` helpers
|
||||
that start a persistent `ssh-agent`, add configured keys, and survive
|
||||
re-login via a socket stored in `~/.ssh/agent.env`. **[medium]**
|
||||
- [x] **`rmhost` glob support** — allow `rmhost '*.example.com'` to remove all
|
||||
matching entries in one call. **[easy]**
|
||||
|
||||
### updates
|
||||
|
||||
- [ ] **Automatic update check age** — store a timestamp in `~/.cache/profile_last_check`;
|
||||
skip the network request in `check_updates -q` if the last check was less
|
||||
than `UPDT_CHECK_INTERVAL` hours ago (configurable, default 24). **[medium]**
|
||||
- [ ] **Changelog display** — after a successful `profile_upgrade`, fetch and
|
||||
display `CHANGELOG.md` entries newer than the previously installed version.
|
||||
**[medium]**
|
||||
|
||||
---
|
||||
|
||||
## Structure
|
||||
|
||||
- [ ] **Helpers must not stay in the environment (do first)** — internal
|
||||
functions (`_` prefix, `helpers/`) are not part of the user interface, yet 16
|
||||
of them are `export -f`, so they leak into the environment of every child
|
||||
process. By design they should only exist where they are needed:
|
||||
- stop exporting helpers by default; unset the ones only needed at load time
|
||||
(as `profile.sh` already does for `parse_conf`, `load_alias`, ...);
|
||||
- a helper stays exported only when an exported public function calls it and
|
||||
must work in a child bash (`bash -c`, `find -exec`, `xargs`), which has to
|
||||
be checked and documented one by one (otherwise that public function is no
|
||||
longer exported either, or waits for the stub mechanism below);
|
||||
- add a check (shellcheck-like script or `bats` test) listing exported
|
||||
functions starting with `_`, so none comes back unnoticed.
|
||||
This is the first step: it does not depend on the lazy loading below, which
|
||||
builds on it. **[medium]**
|
||||
- [ ] **Load code on demand (stubs, lazy loading, optional unloading)** —
|
||||
today every module is sourced at startup (about 63 ms, 109 functions) and 80
|
||||
functions are exported (about 250 KB of environment inherited by every
|
||||
external command, which makes each one about 25 % slower and eats `ARG_MAX`).
|
||||
Replace this by:
|
||||
- *Stubs*: each lazy command is a tiny generated function
|
||||
(`rain() { _lazy rain "$@"; }`), the only thing exported. The stubs come
|
||||
from a command-to-module index, built at upgrade time from a
|
||||
`# provides: ...` header in each module (so `help`, `type` and completion
|
||||
still see every command).
|
||||
- *Lazy loading* (`_try_load`): the first call sources the module and its
|
||||
`helpers/<module>.sh` (and the modules it depends on, declared in the
|
||||
header), then runs the real function (`_exec`). The code stays loaded
|
||||
afterwards. Helpers and implementations are no longer exported, which also
|
||||
removes them from the environment of child shells.
|
||||
- *Optional unloading* (`_free`, e.g. `PROFILE_UNLOAD=1`): after the call,
|
||||
put the stub back instead of unsetting the function. Must be idempotent
|
||||
(Ctrl-C or `return` skip the cleanup), must not free code still referenced
|
||||
by a trap (`_rain_stop`, WINCH handler) or used by a running caller
|
||||
(reference count), and costs about 4 ms per call, so it is not the default.
|
||||
- *Always resident*: `disp`, `conf`, `debug`, `prompt` (and the `DEBUG` timer
|
||||
hook), `lang` shortcuts and `help`. With unloading, state kept across calls
|
||||
(`pwd` pool, current theme, `settrace`) must live in variables that are not
|
||||
freed. Lazy candidates: `compress`,
|
||||
`filefct`, `fun`, `git`, `info`, `net`, `packages`, `processes`, `pwd`,
|
||||
`rain`, `ssh`, `updates`.
|
||||
- *To solve*: `load_conf`/`CONF_*` must stay available at call time; child
|
||||
shells need `MYPATH` and the loader exported to resolve stubs; tracebacks
|
||||
show the loader frames. Do it after the test suite exists, as regressions
|
||||
would be silent. **[hard]**
|
||||
|
||||
---
|
||||
|
||||
## Infrastructure
|
||||
|
||||
- [ ] **Test suite** — add a `test/` directory with `bats` (Bash Automated
|
||||
Testing System) unit tests for pure functions (`expandlist`, `genpwd`,
|
||||
`pwdscore`, `isipv4`, `isipv6`, `parse_conf`, `load_theme`, …). Target for
|
||||
CI. **[hard]**
|
||||
- [ ] **CI pipeline** — Gitea Actions (or similar) job that runs `shellcheck`
|
||||
and the `bats` suite on every push. **[medium]**
|
||||
- [ ] **`profile_status` function** — print a diagnostic summary: installed
|
||||
version, active theme, loaded modules, detected Bash version, interactive /
|
||||
login shell flags, and `profile.conf` path. **[easy]**
|
||||
- [ ] **`profile_uninstall` function** — remove the `source` line from
|
||||
`~/.bashrc` / `~/.profile` and optionally delete the install directory,
|
||||
with a dry-run mode. **[medium]**
|
||||
- [ ] **`disp` syslog integration** — add a `DISP_SYSLOG=<context>` config key that
|
||||
additionally pipes E/W messages to `logger`. **[easy]**
|
||||
- [ ] **XDG base-dir support** — honour `$XDG_CONFIG_HOME` as an alternative
|
||||
location for `profile.conf` so users can keep `~` tidy. **[medium]**
|
||||
**[breaking]**
|
||||
@@ -0,0 +1,152 @@
|
||||
#!/usr/bin/env bash
|
||||
# ------------------------------------------------------------------------------
|
||||
# Copyright (c) 2013-2026 Geoffray Levasseur <fatalerrors@geoffray-levasseur.org>
|
||||
# Protected by the BSD3 license. Please read bellow for details.
|
||||
#
|
||||
# * Redistribution and use in source and binary forms,
|
||||
# * with or without modification, are permitted provided
|
||||
# * that the following conditions are met:
|
||||
# *
|
||||
# * Redistributions of source code must retain the above
|
||||
# * copyright notice, this list of conditions and the
|
||||
# * following disclaimer.
|
||||
# *
|
||||
# * Redistributions in binary form must reproduce the above
|
||||
# * copyright notice, this list of conditions and the following
|
||||
# * disclaimer in the documentation and/or other materials
|
||||
# * provided with the distribution.
|
||||
# *
|
||||
# * Neither the name of the copyright holder nor the names
|
||||
# * of any other contributors may be used to endorse or
|
||||
# * promote products derived from this software without
|
||||
# * specific prior written permission.
|
||||
# *
|
||||
# * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
|
||||
# * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
|
||||
# * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
|
||||
# * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
# * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
|
||||
# * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
# * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
|
||||
# * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
# * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||
# * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
|
||||
# * OF SUCH DAMAGE.
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# Helpers shared by several modules. This file is loaded by profile.sh before
|
||||
# any module (and by a module sourced alone); the helpers private to one module
|
||||
# live in helpers/<module>.sh instead, loaded by the module itself through
|
||||
# _profile_load_helpers.
|
||||
#
|
||||
# Option parsing: the project rule is to never use eval, so _getopt_split and
|
||||
# _trap_restore rebuild argument lists and traps in pure bash.
|
||||
|
||||
# Split the output of getopt into the array whose name is $1, without eval and
|
||||
# without xargs. getopt prints the normalised arguments as one shell-quoted
|
||||
# line (options bare, every argument in single quotes, a quote inside an
|
||||
# argument written '\''), for instance:
|
||||
# -s 'a b' --long 'it'\''s' -- 'file' ''
|
||||
# Quoted text is taken verbatim (newlines included), an empty argument ('')
|
||||
# yields an empty element, and nothing is ever evaluated. The usual call is
|
||||
# local -a _pargs=()
|
||||
# _getopt_split _pargs "$PARSED"
|
||||
# set -- "${_pargs[@]}"
|
||||
# Usage: _getopt_split <array_name> <getopt_output>
|
||||
_getopt_split()
|
||||
{
|
||||
local -n _gs_out="$1"
|
||||
local s="$2" tok="" in_tok=0 run
|
||||
local run_re=$'^[^ \t\n\'\\\\]+'
|
||||
_gs_out=()
|
||||
|
||||
while [[ -n "$s" ]]; do
|
||||
# shellcheck disable=SC1003 # '\' is a literal backslash
|
||||
case "${s:0:1}" in
|
||||
"'")
|
||||
s="${s:1}"
|
||||
if [[ "$s" == *\'* ]]; then
|
||||
tok+="${s%%\'*}"
|
||||
s="${s#*\'}"
|
||||
else
|
||||
# Unterminated quote (never produced by getopt): keep the rest
|
||||
tok+="$s"
|
||||
s=""
|
||||
fi
|
||||
in_tok=1
|
||||
;;
|
||||
'\')
|
||||
tok+="${s:1:1}"
|
||||
s="${s:2}"
|
||||
in_tok=1
|
||||
;;
|
||||
' '|$'\t'|$'\n')
|
||||
(( in_tok )) && _gs_out+=("$tok")
|
||||
tok=""
|
||||
in_tok=0
|
||||
s="${s:1}"
|
||||
;;
|
||||
*)
|
||||
[[ "$s" =~ $run_re ]] || break
|
||||
run="${BASH_REMATCH[0]}"
|
||||
tok+="$run"
|
||||
s="${s:${#run}}"
|
||||
in_tok=1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
(( in_tok )) && _gs_out+=("$tok")
|
||||
return 0
|
||||
}
|
||||
export -f _getopt_split
|
||||
|
||||
# Put back traps saved with "trap -p" (one "trap -- 'command' SIGNAL" line per
|
||||
# trap), without eval. Used to restore the caller's traps after temporarily
|
||||
# replacing them. An empty argument restores nothing.
|
||||
# Usage: _trap_restore "$(trap -p INT TERM)"
|
||||
_trap_restore()
|
||||
{
|
||||
local line
|
||||
local -a _tr_w=()
|
||||
while IFS= read -r line; do
|
||||
[[ "$line" == "trap -- "* ]] || continue
|
||||
_getopt_split _tr_w "$line"
|
||||
# _tr_w = (trap -- <command> <SIGNAL>)
|
||||
[[ ${#_tr_w[@]} -eq 4 ]] && trap -- "${_tr_w[2]}" "${_tr_w[3]}"
|
||||
done <<< "$1"
|
||||
return 0
|
||||
}
|
||||
export -f _trap_restore
|
||||
|
||||
# True when $1 is a non-negative integer. Every value that ends up in an
|
||||
# arithmetic context must pass this first (this includes values coming from the
|
||||
# configuration file): bash evaluates non-numeric strings inside [[ -gt ]] or
|
||||
# $(( )) as expressions, which can run arbitrary commands.
|
||||
# Usage: _is_uint <value>
|
||||
_is_uint()
|
||||
{
|
||||
[[ "$1" =~ ^[0-9]+$ ]]
|
||||
}
|
||||
export -f _is_uint
|
||||
|
||||
# Source the helpers private to a module: helpers/<module name>.sh, found next
|
||||
# to the directory holding the module (profile.d/../helpers), the name being the
|
||||
# module file name without ".sh". A module without helpers file is fine.
|
||||
# Usage: _profile_load_helpers "${BASH_SOURCE[0]}"
|
||||
_profile_load_helpers()
|
||||
{
|
||||
local dir=. name="${1##*/}"
|
||||
[[ "$1" == */* ]] && dir="${1%/*}"
|
||||
name="$dir/../helpers/${name%.sh}.sh"
|
||||
[[ -r "$name" ]] || return 0
|
||||
# shellcheck source=/dev/null
|
||||
. "$name" || printf "[ Warning ] Failed to source helpers: %s\n" "$name" >&2
|
||||
}
|
||||
export -f _profile_load_helpers
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
|
||||
# EOF
|
||||
@@ -0,0 +1,52 @@
|
||||
#!/usr/bin/env bash
|
||||
# ------------------------------------------------------------------------------
|
||||
# Copyright (c) 2013-2026 Geoffray Levasseur <fatalerrors@geoffray-levasseur.org>
|
||||
# Protected by the BSD3 license. Please read bellow for details.
|
||||
#
|
||||
# * Redistribution and use in source and binary forms,
|
||||
# * with or without modification, are permitted provided
|
||||
# * that the following conditions are met:
|
||||
# *
|
||||
# * Redistributions of source code must retain the above
|
||||
# * copyright notice, this list of conditions and the
|
||||
# * following disclaimer.
|
||||
# *
|
||||
# * Redistributions in binary form must reproduce the above
|
||||
# * copyright notice, this list of conditions and the following
|
||||
# * disclaimer in the documentation and/or other materials
|
||||
# * provided with the distribution.
|
||||
# *
|
||||
# * Neither the name of the copyright holder nor the names
|
||||
# * of any other contributors may be used to endorse or
|
||||
# * promote products derived from this software without
|
||||
# * specific prior written permission.
|
||||
# *
|
||||
# * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
|
||||
# * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
|
||||
# * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
|
||||
# * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
# * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
|
||||
# * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
# * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
|
||||
# * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
# * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||
# * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
|
||||
# * OF SUCH DAMAGE.
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
# Private helpers of profile.d/conf.sh, loaded by the module itself.
|
||||
|
||||
# Print the path of the configuration file the conf_* functions work on:
|
||||
# the user file ($HOME/.profile.conf) when it exists, otherwise the installation
|
||||
# file ($PROFILE_CONF, defaulting to $MYPATH/profile.conf). The returned path
|
||||
# may not exist yet.
|
||||
# Usage: _conf_path
|
||||
_conf_path()
|
||||
{
|
||||
if [[ -f "$HOME/.profile.conf" ]]; then
|
||||
printf '%s\n' "$HOME/.profile.conf"
|
||||
else
|
||||
printf '%s\n' "${PROFILE_CONF:-${MYPATH}/profile.conf}"
|
||||
fi
|
||||
}
|
||||
export -f _conf_path
|
||||
@@ -0,0 +1,89 @@
|
||||
#!/usr/bin/env bash
|
||||
# ------------------------------------------------------------------------------
|
||||
# Copyright (c) 2013-2026 Geoffray Levasseur <fatalerrors@geoffray-levasseur.org>
|
||||
# Protected by the BSD3 license. Please read bellow for details.
|
||||
#
|
||||
# * Redistribution and use in source and binary forms,
|
||||
# * with or without modification, are permitted provided
|
||||
# * that the following conditions are met:
|
||||
# *
|
||||
# * Redistributions of source code must retain the above
|
||||
# * copyright notice, this list of conditions and the
|
||||
# * following disclaimer.
|
||||
# *
|
||||
# * Redistributions in binary form must reproduce the above
|
||||
# * copyright notice, this list of conditions and the following
|
||||
# * disclaimer in the documentation and/or other materials
|
||||
# * provided with the distribution.
|
||||
# *
|
||||
# * Neither the name of the copyright holder nor the names
|
||||
# * of any other contributors may be used to endorse or
|
||||
# * promote products derived from this software without
|
||||
# * specific prior written permission.
|
||||
# *
|
||||
# * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
|
||||
# * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
|
||||
# * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
|
||||
# * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
# * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
|
||||
# * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
# * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
|
||||
# * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
# * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||
# * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
|
||||
# * OF SUCH DAMAGE.
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
# Private helpers of profile.d/fun.sh, loaded by the module itself.
|
||||
|
||||
# Convert a number of milliseconds into a seconds string usable by sleep
|
||||
# (1500 -> 1.500), in pure bash (no awk fork per call).
|
||||
# Usage: _fun_ms_to_s <ms>
|
||||
_fun_ms_to_s()
|
||||
{
|
||||
printf '%d.%03d' $(( $1 / 1000 )) $(( $1 % 1000 ))
|
||||
}
|
||||
export -f _fun_ms_to_s
|
||||
|
||||
# Fail with a message unless a terminal is available: the demos wait for a key
|
||||
# read from /dev/tty to stop.
|
||||
# Usage: _fun_need_tty <command-name>
|
||||
_fun_need_tty()
|
||||
{
|
||||
if ! { : </dev/tty; } 2>/dev/null; then
|
||||
disp E "$1: a terminal is required (it runs until a key is pressed)."
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
export -f _fun_need_tty
|
||||
|
||||
# Wait for a key press, then stop the background job $1 and its children.
|
||||
# The job runs as "( ... ) &"; with job control (interactive shell) it is the
|
||||
# leader of its own process group, which is signalled as a whole. Without job
|
||||
# control that group does not exist, so its direct children (the pipeline) are
|
||||
# killed first, then the subshell. SIGINT is trapped meanwhile so that Ctrl-C
|
||||
# stops the job like a key press instead of aborting the caller and leaving it
|
||||
# running. The wait is a polling loop of short timed reads: a blocking read is
|
||||
# restarted by bash after a trapped signal and would never notice the Ctrl-C.
|
||||
# Usage: _fun_stop_on_key <pid>
|
||||
_fun_stop_on_key()
|
||||
{
|
||||
local pid=$1 old_int interrupted=0
|
||||
old_int=$(trap -p INT)
|
||||
trap 'interrupted=1' INT
|
||||
while (( ! interrupted )); do
|
||||
IFS= read -r -n 1 -s -t 0.3 _ </dev/tty && break
|
||||
# Status above 128 is just the timeout; anything else (such as end of
|
||||
# input on the terminal) ends the wait.
|
||||
(( $? > 128 )) || break
|
||||
done
|
||||
kill -- -"$pid" 2>/dev/null || {
|
||||
pkill -P "$pid" 2>/dev/null
|
||||
kill "$pid" 2>/dev/null
|
||||
}
|
||||
wait "$pid" 2>/dev/null
|
||||
trap - INT
|
||||
_trap_restore "$old_int"
|
||||
return 0
|
||||
}
|
||||
export -f _fun_stop_on_key
|
||||
@@ -0,0 +1,99 @@
|
||||
#!/usr/bin/env bash
|
||||
# ------------------------------------------------------------------------------
|
||||
# Copyright (c) 2013-2026 Geoffray Levasseur <fatalerrors@geoffray-levasseur.org>
|
||||
# Protected by the BSD3 license. Please read bellow for details.
|
||||
#
|
||||
# * Redistribution and use in source and binary forms,
|
||||
# * with or without modification, are permitted provided
|
||||
# * that the following conditions are met:
|
||||
# *
|
||||
# * Redistributions of source code must retain the above
|
||||
# * copyright notice, this list of conditions and the
|
||||
# * following disclaimer.
|
||||
# *
|
||||
# * Redistributions in binary form must reproduce the above
|
||||
# * copyright notice, this list of conditions and the following
|
||||
# * disclaimer in the documentation and/or other materials
|
||||
# * provided with the distribution.
|
||||
# *
|
||||
# * Neither the name of the copyright holder nor the names
|
||||
# * of any other contributors may be used to endorse or
|
||||
# * promote products derived from this software without
|
||||
# * specific prior written permission.
|
||||
# *
|
||||
# * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
|
||||
# * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
|
||||
# * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
|
||||
# * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
# * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
|
||||
# * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
# * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
|
||||
# * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
# * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||
# * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
|
||||
# * OF SUCH DAMAGE.
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
# Private helpers of profile.d/git.sh, loaded by the module itself.
|
||||
|
||||
# Internal helper: ensure git is available and cwd is a git worktree
|
||||
_git_require_repo()
|
||||
{
|
||||
if ! command -v git >/dev/null 2>&1; then
|
||||
disp E "git command not found."
|
||||
return 1
|
||||
fi
|
||||
|
||||
if ! git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
|
||||
disp E "Current directory is not inside a git repository."
|
||||
return 1
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
export -f _git_require_repo
|
||||
|
||||
# Internal helper: return default branch from remote HEAD, fallback to config
|
||||
_git_default_branch()
|
||||
{
|
||||
local remote="${1:-$GIT_DEFAULT_REMOTE}"
|
||||
local head
|
||||
|
||||
head=$(git symbolic-ref --quiet --short "refs/remotes/${remote}/HEAD" 2>/dev/null) || true
|
||||
if [[ -n $head ]]; then
|
||||
printf "%s\n" "${head#"${remote}"/}"
|
||||
return 0
|
||||
fi
|
||||
|
||||
printf "%s\n" "$GIT_MAIN_BRANCH"
|
||||
return 0
|
||||
}
|
||||
export -f _git_default_branch
|
||||
|
||||
# Internal helper: print the name of the checked-out branch; fail with an error
|
||||
# message on a detached HEAD (commands that push or rebase need a branch).
|
||||
_git_current_branch()
|
||||
{
|
||||
local b
|
||||
b=$(git symbolic-ref --quiet --short HEAD 2>/dev/null) || {
|
||||
disp E "HEAD is detached: check out a branch first."
|
||||
return 1
|
||||
}
|
||||
printf "%s\n" "$b"
|
||||
}
|
||||
export -f _git_current_branch
|
||||
|
||||
# Internal helper: print "<remote><TAB><branch>" for the remote branch tracked
|
||||
# by local branch $1, read from its configuration (branch.<name>.remote/.merge).
|
||||
# Unlike parsing the "origin/feature/x" form given by @{u}, this is exact for
|
||||
# remote and branch names containing slashes. Fails when nothing is tracked or
|
||||
# when the branch tracks another local branch (remote ".").
|
||||
_git_tracking()
|
||||
{
|
||||
local branch=$1 remote merge
|
||||
remote=$(git config --get "branch.${branch}.remote") || return 1
|
||||
merge=$(git config --get "branch.${branch}.merge") || return 1
|
||||
[[ $remote == "." ]] && return 1
|
||||
printf "%s\t%s\n" "$remote" "${merge#refs/heads/}"
|
||||
}
|
||||
export -f _git_tracking
|
||||
@@ -0,0 +1,72 @@
|
||||
#!/usr/bin/env bash
|
||||
# ------------------------------------------------------------------------------
|
||||
# Copyright (c) 2013-2026 Geoffray Levasseur <fatalerrors@geoffray-levasseur.org>
|
||||
# Protected by the BSD3 license. Please read bellow for details.
|
||||
#
|
||||
# * Redistribution and use in source and binary forms,
|
||||
# * with or without modification, are permitted provided
|
||||
# * that the following conditions are met:
|
||||
# *
|
||||
# * Redistributions of source code must retain the above
|
||||
# * copyright notice, this list of conditions and the
|
||||
# * following disclaimer.
|
||||
# *
|
||||
# * Redistributions in binary form must reproduce the above
|
||||
# * copyright notice, this list of conditions and the following
|
||||
# * disclaimer in the documentation and/or other materials
|
||||
# * provided with the distribution.
|
||||
# *
|
||||
# * Neither the name of the copyright holder nor the names
|
||||
# * of any other contributors may be used to endorse or
|
||||
# * promote products derived from this software without
|
||||
# * specific prior written permission.
|
||||
# *
|
||||
# * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
|
||||
# * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
|
||||
# * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
|
||||
# * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
# * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
|
||||
# * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
# * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
|
||||
# * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
# * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||
# * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
|
||||
# * OF SUCH DAMAGE.
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
# Private helpers of profile.d/packages.sh, loaded by the module itself.
|
||||
|
||||
# Internal helper: run a package manager command, with root privileges when
|
||||
# needed (sudo, else doas), or only display it in dry-run mode.
|
||||
# Usage: _pkg_run <needs_root 0|1> <dry_run 0|1> <command> [args...]
|
||||
_pkg_run()
|
||||
{
|
||||
local needs_root="$1" dry="$2"
|
||||
shift 2
|
||||
local -a prefix=()
|
||||
|
||||
if (( needs_root )) && [[ $EUID -ne 0 ]]; then
|
||||
if command -v sudo >/dev/null 2>&1; then
|
||||
prefix=(sudo)
|
||||
elif command -v doas >/dev/null 2>&1; then
|
||||
prefix=(doas)
|
||||
elif (( ! dry )); then
|
||||
disp E "Root privileges are required and neither sudo nor doas is available."
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
|
||||
if (( dry )); then
|
||||
# %q gives a copy-pasteable command line
|
||||
printf '%q ' "${prefix[@]}" "$@"
|
||||
printf '\n'
|
||||
return 0
|
||||
fi
|
||||
|
||||
if ! command -v "$1" >/dev/null 2>&1; then
|
||||
disp E "'$1' is not installed."
|
||||
return 1
|
||||
fi
|
||||
"${prefix[@]}" "$@"
|
||||
}
|
||||
export -f _pkg_run
|
||||
@@ -0,0 +1,90 @@
|
||||
#!/usr/bin/env bash
|
||||
# ------------------------------------------------------------------------------
|
||||
# Copyright (c) 2013-2026 Geoffray Levasseur <fatalerrors@geoffray-levasseur.org>
|
||||
# Protected by the BSD3 license. Please read bellow for details.
|
||||
#
|
||||
# * Redistribution and use in source and binary forms,
|
||||
# * with or without modification, are permitted provided
|
||||
# * that the following conditions are met:
|
||||
# *
|
||||
# * Redistributions of source code must retain the above
|
||||
# * copyright notice, this list of conditions and the
|
||||
# * following disclaimer.
|
||||
# *
|
||||
# * Redistributions in binary form must reproduce the above
|
||||
# * copyright notice, this list of conditions and the following
|
||||
# * disclaimer in the documentation and/or other materials
|
||||
# * provided with the distribution.
|
||||
# *
|
||||
# * Neither the name of the copyright holder nor the names
|
||||
# * of any other contributors may be used to endorse or
|
||||
# * promote products derived from this software without
|
||||
# * specific prior written permission.
|
||||
# *
|
||||
# * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
|
||||
# * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
|
||||
# * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
|
||||
# * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
# * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
|
||||
# * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
# * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
|
||||
# * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
# * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||
# * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
|
||||
# * OF SUCH DAMAGE.
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
# Private helpers of profile.d/processes.sh, loaded by the module itself.
|
||||
|
||||
# Internal helper: take the kill-style signal arguments (-9, -KILL, -SIGHUP)
|
||||
# out of an argument list. getopt rejects them as unknown options, so they must
|
||||
# be removed before it runs. They are recognised anywhere before a "--"
|
||||
# (digits, or upper-case names, which cannot clash with the lower-case options
|
||||
# of the callers); everything after "--" is left untouched.
|
||||
# Usage: _proc_split_signals <signals_array> <rest_array> [args...]
|
||||
_proc_split_signals()
|
||||
{
|
||||
local -n _pss_sigs="$1" _pss_rest="$2"
|
||||
shift 2
|
||||
_pss_sigs=()
|
||||
_pss_rest=()
|
||||
local a
|
||||
while [[ $# -gt 0 ]]; do
|
||||
a="$1"
|
||||
shift
|
||||
if [[ $a == "--" ]]; then
|
||||
_pss_rest+=(-- "$@")
|
||||
break
|
||||
fi
|
||||
if [[ $a =~ ^-([0-9]+|SIG[A-Z0-9+-]+|[A-Z][A-Z0-9+-]*)$ ]]; then
|
||||
_pss_sigs+=("$a")
|
||||
else
|
||||
_pss_rest+=("$a")
|
||||
fi
|
||||
done
|
||||
}
|
||||
export -f _proc_split_signals
|
||||
|
||||
# Internal helper of kt: send a signal to a process and, first, to all its
|
||||
# descendants (depth first, so a parent never outlives the children it could
|
||||
# respawn from). A failure does not stop the others; the status is non-zero if
|
||||
# any kill failed.
|
||||
# Usage: _kt_tree <dry_run 0|1> <pid> [kill_args...]
|
||||
_kt_tree()
|
||||
{
|
||||
local dry="$1" pid="$2" child rc=0
|
||||
shift 2
|
||||
for child in $(pgrep -P "$pid" 2>/dev/null); do
|
||||
_kt_tree "$dry" "$child" "$@" || rc=1
|
||||
done
|
||||
|
||||
if (( dry )); then
|
||||
printf "DRY-RUN: "
|
||||
printf "%q " kill "$@" "$pid"
|
||||
printf "\n"
|
||||
else
|
||||
kill "$@" "$pid" || rc=1
|
||||
fi
|
||||
return "$rc"
|
||||
}
|
||||
export -f _kt_tree
|
||||
@@ -0,0 +1,277 @@
|
||||
#!/usr/bin/env bash
|
||||
# ------------------------------------------------------------------------------
|
||||
# Copyright (c) 2013-2026 Geoffray Levasseur <fatalerrors@geoffray-levasseur.org>
|
||||
# Protected by the BSD3 license. Please read bellow for details.
|
||||
#
|
||||
# * Redistribution and use in source and binary forms,
|
||||
# * with or without modification, are permitted provided
|
||||
# * that the following conditions are met:
|
||||
# *
|
||||
# * Redistributions of source code must retain the above
|
||||
# * copyright notice, this list of conditions and the
|
||||
# * following disclaimer.
|
||||
# *
|
||||
# * Redistributions in binary form must reproduce the above
|
||||
# * copyright notice, this list of conditions and the following
|
||||
# * disclaimer in the documentation and/or other materials
|
||||
# * provided with the distribution.
|
||||
# *
|
||||
# * Neither the name of the copyright holder nor the names
|
||||
# * of any other contributors may be used to endorse or
|
||||
# * promote products derived from this software without
|
||||
# * specific prior written permission.
|
||||
# *
|
||||
# * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
|
||||
# * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
|
||||
# * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
|
||||
# * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
# * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
|
||||
# * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
# * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
|
||||
# * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
# * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||
# * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
|
||||
# * OF SUCH DAMAGE.
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
# Private helpers of profile.d/prompt.sh, loaded by the module itself.
|
||||
|
||||
# Colour variable names defined by disp.sh. They are the only names a theme or
|
||||
# the [prompt] configuration may reference (as $Name or ${Name}), and the only
|
||||
# palette variables a theme may redefine.
|
||||
_PROMPT_COLOR_NAMES=(
|
||||
Black Red Green Yellow Blue Purple Cyan White
|
||||
BBlack BRed BGreen BYellow BBlue BPurple BCyan BWhite
|
||||
UBlack URed UGreen UYellow UBlue UPurple UCyan UWhite
|
||||
On_Black On_Red On_Green On_Yellow On_Blue On_Purple On_Cyan On_White
|
||||
IBlack IRed IGreen IYellow IBlue IPurple ICyan IWhite
|
||||
BIBlack BIRed BIGreen BIYellow BIBlue BIPurple BICyan BIWhite
|
||||
On_IBlack On_IRed On_IGreen On_IYellow On_IBlue On_IPurple On_ICyan On_IWhite
|
||||
DEFAULTFG DEFAULTBG DEFAULTCOL RESETCOL
|
||||
)
|
||||
# The same names as an ERE alternation (Black|Red|...)
|
||||
_PROMPT_COLOR_RE=""
|
||||
for _pc_name in "${_PROMPT_COLOR_NAMES[@]}"; do
|
||||
_PROMPT_COLOR_RE+="|${_pc_name}"
|
||||
done
|
||||
_PROMPT_COLOR_RE="${_PROMPT_COLOR_RE#|}"
|
||||
unset _pc_name
|
||||
|
||||
# Check a colour value and put its resolved form in _PROMPT_RESOLVED.
|
||||
# A value is a sequence of blocks, each one either
|
||||
# $ColorName or ${ColorName} — colour variable from disp.sh (resolved by
|
||||
# indirection, never evaluated)
|
||||
# \e[...m or \033[...m — raw ANSI escape literal
|
||||
# Anything else is refused (return 1): colours end up inside PS1, so they must
|
||||
# not be able to carry anything else than colour codes.
|
||||
# Usage: _prompt_resolve_color <value>
|
||||
_prompt_resolve_color()
|
||||
{
|
||||
local rest="$1" out="" name
|
||||
# shellcheck disable=SC2016 # a regular expression, the $ must stay literal
|
||||
local ref_re='^\$(\{('"$_PROMPT_COLOR_RE"')\}|('"$_PROMPT_COLOR_RE"'))'
|
||||
local ansi_re='^(\\e|\\033)\[[0-9;]*m'
|
||||
|
||||
[[ -n $rest ]] || return 1
|
||||
while [[ -n $rest ]]; do
|
||||
if [[ $rest =~ $ref_re ]]; then
|
||||
name="${BASH_REMATCH[2]:-${BASH_REMATCH[3]}}"
|
||||
out+="${!name:-}"
|
||||
elif [[ $rest =~ $ansi_re ]]; then
|
||||
out+="${BASH_REMATCH[0]}"
|
||||
else
|
||||
return 1
|
||||
fi
|
||||
rest="${rest:${#BASH_REMATCH[0]}}"
|
||||
done
|
||||
_PROMPT_RESOLVED="$out"
|
||||
}
|
||||
|
||||
# Path of the theme file for a bare name (searched in the theme directory) or an
|
||||
# explicit path (anything containing a slash).
|
||||
# Usage: _prompt_theme_file <theme_name_or_path> [theme_dir]
|
||||
_prompt_theme_file()
|
||||
{
|
||||
local name="$1" dir="${2:-${PROMPT_THEME_DIR:-$MYPATH/profile.d/themes}}"
|
||||
if [[ "$name" == */* ]]; then
|
||||
printf '%s' "$name"
|
||||
else
|
||||
printf '%s' "$dir/${name}.theme"
|
||||
fi
|
||||
}
|
||||
|
||||
# Apply the PROMPT_COLOR_* keys of the [prompt] section on top of the current
|
||||
# colours, so they win over the theme. Their values may use $ColorName, which
|
||||
# parse_conf leaves untouched while the colour variables do not exist yet.
|
||||
# A value that is not a valid colour is reported and not applied.
|
||||
# Usage: _prompt_apply_overrides
|
||||
_prompt_apply_overrides()
|
||||
{
|
||||
# shellcheck disable=SC2154 # CONF_prompt is filled by parse_conf (profile.sh)
|
||||
declare -p CONF_prompt >/dev/null 2>&1 || return 0
|
||||
|
||||
local k
|
||||
for k in "${!CONF_prompt[@]}"; do
|
||||
[[ $k == PROMPT_COLOR_* ]] || continue
|
||||
if _prompt_resolve_color "${CONF_prompt[$k]}"; then
|
||||
export "$k=$_PROMPT_RESOLVED"
|
||||
else
|
||||
printf "[ Warning ] [prompt] %s: invalid colour value '%s', ignoring.\n" \
|
||||
"$k" "${CONF_prompt[$k]}" >&2
|
||||
# load_conf exported the raw text: do not keep it as a colour
|
||||
[[ "${!k:-}" == "${CONF_prompt[$k]}" ]] && unset "$k"
|
||||
fi
|
||||
done
|
||||
return 0
|
||||
}
|
||||
|
||||
# Switch to a theme from a clean state: default palette, no PROMPT_COLOR_*
|
||||
# left over from the previous theme (a partial theme must not inherit them),
|
||||
# the theme, then the [prompt] overrides. The theme file is checked first, so a
|
||||
# wrong name changes nothing.
|
||||
# Usage: _prompt_load_theme_clean <theme_name_or_path> [theme_dir]
|
||||
_prompt_load_theme_clean()
|
||||
{
|
||||
local file v
|
||||
file=$(_prompt_theme_file "$1" "${2:-}")
|
||||
if [[ ! -f "$file" || ! -r "$file" ]]; then
|
||||
printf "[ Warning ] set_theme: theme file not found: %s\n" "$file" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
set_colors
|
||||
for v in ${!PROMPT_COLOR_@}; do
|
||||
unset "$v"
|
||||
done
|
||||
load_theme "$1" "${2:-}" || return 1
|
||||
_prompt_apply_overrides
|
||||
}
|
||||
|
||||
# Put the current time, in microseconds since the epoch, in _timer_us.
|
||||
# EPOCHREALTIME (bash >= 5) costs no process; its decimal separator follows the
|
||||
# locale ("." or ","), hence the removal of both. Otherwise "date +%s%N" is
|
||||
# used (it prints a literal "N" where nanoseconds are not supported, which the
|
||||
# digit check rejects), then plain seconds.
|
||||
_timer_now()
|
||||
{
|
||||
if [[ -n "${EPOCHREALTIME:-}" ]]; then
|
||||
_timer_us="${EPOCHREALTIME//[.,]/}"
|
||||
else
|
||||
local t
|
||||
t=$(date +%s%N 2>/dev/null)
|
||||
if [[ $t =~ ^[0-9]{16,}$ ]]; then
|
||||
_timer_us=$(( t / 1000 ))
|
||||
else
|
||||
_timer_us=$(( $(date +%s) * 1000000 ))
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
# Prompt segment: set _PROMPT_SEG to the git branch (or short commit when
|
||||
# detached) with dirty/ahead/behind markers; empty outside a repository or when
|
||||
# disabled by PROMPT_SHOW_GIT / PROMPT_SHOW_GIT_STATUS.
|
||||
_prompt_seg_git()
|
||||
{
|
||||
_PROMPT_SEG=""
|
||||
# Fast path: skip git lookup when feature is disabled.
|
||||
[[ "${PROMPT_SHOW_GIT:-1}" == "0" ]] && return 0
|
||||
|
||||
local branch
|
||||
branch=$(git symbolic-ref --quiet --short HEAD 2>/dev/null) || \
|
||||
branch=$(git rev-parse --short HEAD 2>/dev/null) || return 0
|
||||
|
||||
local dirty="" sync="" timed_out=0
|
||||
|
||||
if [[ "${PROMPT_SHOW_GIT_STATUS:-1}" != "0" ]]; then
|
||||
# Build a timeout wrapper if the 'timeout' command is available;
|
||||
# fall back to direct execution on systems that lack it.
|
||||
local -a _tw=()
|
||||
command -v timeout >/dev/null 2>&1 && _tw=(timeout "${PROMPT_GIT_TIMEOUT:-2}")
|
||||
|
||||
# One call for everything: --branch gives the "# branch.ab +A -B" line
|
||||
# (only when an upstream exists) and each changed tracked file gives a
|
||||
# line starting with 1, 2 or u. Untracked files are not counted.
|
||||
# GIT_OPTIONAL_LOCKS=0 keeps git from taking the index lock to refresh
|
||||
# it, which would make a concurrent "git commit" fail.
|
||||
local out ec
|
||||
out=$(GIT_OPTIONAL_LOCKS=0 "${_tw[@]}" git status --porcelain=v2 --branch \
|
||||
--untracked-files=no --ignore-submodules 2>/dev/null)
|
||||
ec=$?
|
||||
if [[ $ec -eq 124 ]]; then
|
||||
timed_out=1
|
||||
else
|
||||
[[ $'\n'"$out" == *$'\n'[12u]\ * ]] && dirty="*"
|
||||
if [[ "$out" =~ \#\ branch\.ab\ \+([0-9]+)\ -([0-9]+) ]]; then
|
||||
if [[ "${BASH_REMATCH[1]}" -gt 0 || "${BASH_REMATCH[2]}" -gt 0 ]]; then
|
||||
sync=" +${BASH_REMATCH[1]}/-${BASH_REMATCH[2]}"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ $timed_out -eq 1 ]]; then
|
||||
_PROMPT_SEG="git:${branch}?"
|
||||
else
|
||||
_PROMPT_SEG="git:${branch}${dirty}${sync}"
|
||||
fi
|
||||
}
|
||||
|
||||
# Prompt segment: set _PROMPT_SEG to the active conda environment, if any.
|
||||
_prompt_seg_conda()
|
||||
{
|
||||
_PROMPT_SEG=""
|
||||
[[ "${PROMPT_SHOW_CONDA:-1}" == "0" ]] && return 0
|
||||
[[ -z "${CONDA_DEFAULT_ENV:-}" ]] && return 0
|
||||
_PROMPT_SEG="conda:${CONDA_DEFAULT_ENV}"
|
||||
}
|
||||
|
||||
# Prompt segment: set _PROMPT_SEG to the active Python virtualenv name, unless
|
||||
# a conda environment (which sets its own) is active.
|
||||
_prompt_seg_venv()
|
||||
{
|
||||
_PROMPT_SEG=""
|
||||
[[ "${PROMPT_SHOW_VENV:-1}" == "0" ]] && return 0
|
||||
[[ -n "${CONDA_DEFAULT_ENV:-}" ]] && return 0
|
||||
[[ -z "${VIRTUAL_ENV:-}" ]] && return 0
|
||||
_PROMPT_SEG="venv:${VIRTUAL_ENV##*/}"
|
||||
}
|
||||
|
||||
# Prompt segment: set _PROMPT_SEG to the session kind tags (ssh, tmux, screen)
|
||||
# joined with '+', empty for a plain local session.
|
||||
_prompt_seg_session()
|
||||
{
|
||||
_PROMPT_SEG=""
|
||||
[[ "${PROMPT_SHOW_SESSION:-1}" == "0" ]] && return 0
|
||||
|
||||
local -a tags=()
|
||||
[[ -n "${SSH_CONNECTION:-}" ]] && tags+=("ssh")
|
||||
[[ -n "${TMUX:-}" ]] && tags+=("tmux")
|
||||
[[ -n "${STY:-}" ]] && tags+=("screen")
|
||||
[[ ${#tags[@]} -eq 0 ]] && return 0
|
||||
|
||||
local out="${tags[0]}" i
|
||||
for ((i=1; i<${#tags[@]}; ++i)); do
|
||||
out+="+${tags[i]}"
|
||||
done
|
||||
_PROMPT_SEG="$out"
|
||||
}
|
||||
|
||||
# Append the current _PROMPT_SEG to the context text _PROMPT_CTX (" | " between
|
||||
# segments). An "if" keeps the status at 0 when the segment is empty.
|
||||
_prompt_ctx_add()
|
||||
{
|
||||
if [[ -n "$_PROMPT_SEG" ]]; then
|
||||
if [[ -n "$_PROMPT_CTX" ]]; then
|
||||
_PROMPT_CTX+=" | $_PROMPT_SEG"
|
||||
else
|
||||
_PROMPT_CTX="$_PROMPT_SEG"
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
# Store the exit status of the user's last command (first command of a
|
||||
# PROMPT_COMMAND that also contains other commands, see profile.sh).
|
||||
_profile_save_status()
|
||||
{
|
||||
_profile_status=$?
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
#!/usr/bin/env bash
|
||||
# ------------------------------------------------------------------------------
|
||||
# Copyright (c) 2013-2026 Geoffray Levasseur <fatalerrors@geoffray-levasseur.org>
|
||||
# Protected by the BSD3 license. Please read bellow for details.
|
||||
#
|
||||
# * Redistribution and use in source and binary forms,
|
||||
# * with or without modification, are permitted provided
|
||||
# * that the following conditions are met:
|
||||
# *
|
||||
# * Redistributions of source code must retain the above
|
||||
# * copyright notice, this list of conditions and the
|
||||
# * following disclaimer.
|
||||
# *
|
||||
# * Redistributions in binary form must reproduce the above
|
||||
# * copyright notice, this list of conditions and the following
|
||||
# * disclaimer in the documentation and/or other materials
|
||||
# * provided with the distribution.
|
||||
# *
|
||||
# * Neither the name of the copyright holder nor the names
|
||||
# * of any other contributors may be used to endorse or
|
||||
# * promote products derived from this software without
|
||||
# * specific prior written permission.
|
||||
# *
|
||||
# * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
|
||||
# * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
|
||||
# * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
|
||||
# * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
# * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
|
||||
# * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
# * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
|
||||
# * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
# * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||
# * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
|
||||
# * OF SUCH DAMAGE.
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
# Private helpers of profile.d/pwd.sh, loaded by the module itself.
|
||||
|
||||
# Pool of random words read from /dev/urandom, refilled by _pwd_rand.
|
||||
_PWD_POOL=()
|
||||
|
||||
# Internal helper: put in _PWD_RAND an integer uniformly distributed in
|
||||
# [0, $1), for 1 <= $1 <= 2^32. $RANDOM is NOT usable for passwords: it only
|
||||
# has 15 bits, a state seeded from the time and the PID, and is predictable.
|
||||
# The source is $SRANDOM (bash >= 5.1, from the kernel's random generator), or
|
||||
# else /dev/urandom read through od, 256 words at a time to limit the number
|
||||
# of processes. Values above the largest multiple of $1 are rejected, so that
|
||||
# "% $1" introduces no bias.
|
||||
# Usage: _pwd_rand <max>
|
||||
_pwd_rand()
|
||||
{
|
||||
local max="$1" limit r
|
||||
limit=$(( 4294967296 - 4294967296 % max ))
|
||||
while :; do
|
||||
if [[ -n "${SRANDOM+x}" ]]; then
|
||||
r="$SRANDOM"
|
||||
else
|
||||
if (( ${#_PWD_POOL[@]} == 0 )); then
|
||||
# shellcheck disable=SC2207 # whitespace-separated numbers
|
||||
_PWD_POOL=( $(od -An -v -N1024 -tu4 /dev/urandom 2>/dev/null) )
|
||||
if (( ${#_PWD_POOL[@]} == 0 )); then
|
||||
disp E "No source of random numbers available (/dev/urandom)."
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
r="${_PWD_POOL[-1]}"
|
||||
unset '_PWD_POOL[-1]'
|
||||
fi
|
||||
if (( r < limit )); then
|
||||
_PWD_RAND=$(( r % max ))
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
}
|
||||
+468
@@ -0,0 +1,468 @@
|
||||
#!/usr/bin/env bash
|
||||
# ------------------------------------------------------------------------------
|
||||
# Copyright (c) 2013-2026 Geoffray Levasseur <fatalerrors@geoffray-levasseur.org>
|
||||
# Protected by the BSD3 license. Please read bellow for details.
|
||||
#
|
||||
# * Redistribution and use in source and binary forms,
|
||||
# * with or without modification, are permitted provided
|
||||
# * that the following conditions are met:
|
||||
# *
|
||||
# * Redistributions of source code must retain the above
|
||||
# * copyright notice, this list of conditions and the
|
||||
# * following disclaimer.
|
||||
# *
|
||||
# * Redistributions in binary form must reproduce the above
|
||||
# * copyright notice, this list of conditions and the following
|
||||
# * disclaimer in the documentation and/or other materials
|
||||
# * provided with the distribution.
|
||||
# *
|
||||
# * Neither the name of the copyright holder nor the names
|
||||
# * of any other contributors may be used to endorse or
|
||||
# * promote products derived from this software without
|
||||
# * specific prior written permission.
|
||||
# *
|
||||
# * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
|
||||
# * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
|
||||
# * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
|
||||
# * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
# * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
|
||||
# * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
# * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
|
||||
# * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
# * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||
# * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
|
||||
# * OF SUCH DAMAGE.
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
# Private helpers of profile.d/rain.sh, loaded by the module itself.
|
||||
|
||||
# Fill RAIN_ENGINE_COLORS with the escape sequences of the drop colours (head to
|
||||
# tail) of the colour theme $1: a 20-step 24-bit gradient when the terminal
|
||||
# supports truecolor, a short 256-colour palette otherwise.
|
||||
# Usage: _rain_build_colors <colour_name>
|
||||
_rain_build_colors()
|
||||
{
|
||||
# Case-insensitive: the configuration documents capitalised names (Green)
|
||||
local base_color="${1,,}"
|
||||
local i
|
||||
RAIN_ENGINE_COLORS=()
|
||||
|
||||
case "$base_color" in
|
||||
green|blue|red|yellow|cyan|white) ;;
|
||||
*)
|
||||
disp E "Unknown color: ${1} (supported: green, blue, red, yellow, cyan, white)."
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
|
||||
local use_truecolor=0
|
||||
# term_set() already sets TERM=*-direct when truecolor is available;
|
||||
# honour COLORTERM as a belt-and-suspenders fallback.
|
||||
[[ "$TERM" == *direct* || "${COLORTERM:-}" == "truecolor" || "${COLORTERM:-}" == "24bit" ]] && use_truecolor=1
|
||||
|
||||
if (( use_truecolor )); then
|
||||
# 24-bit gradient from a near-black shade to a vivid hue.
|
||||
# 20 steps provide smooth depth variation across simultaneous drops.
|
||||
local steps=20
|
||||
local r1 g1 b1 r2 g2 b2
|
||||
case "${base_color}" in
|
||||
green) r1=0; g1=12; b1=2; r2=50; g2=255; b2=90 ;;
|
||||
blue) r1=0; g1=5; b1=25; r2=60; g2=140; b2=255 ;;
|
||||
red) r1=15; g1=0; b1=0; r2=255; g2=40; b2=40 ;;
|
||||
yellow) r1=25; g1=18; b1=0; r2=255; g2=240; b2=50 ;;
|
||||
cyan) r1=0; g1=18; b1=18; r2=50; g2=255; b2=240 ;;
|
||||
*) r1=40; g1=40; b1=45; r2=220; g2=220; b2=255 ;;
|
||||
esac
|
||||
|
||||
local r g b
|
||||
for ((i = 0; i < steps; i++)); do
|
||||
r=$(( r1 + (r2 - r1) * i / (steps - 1) ))
|
||||
g=$(( g1 + (g2 - g1) * i / (steps - 1) ))
|
||||
b=$(( b1 + (b2 - b1) * i / (steps - 1) ))
|
||||
RAIN_ENGINE_COLORS+=("\e[38;2;${r};${g};${b}m")
|
||||
done
|
||||
else
|
||||
# Fallback: 256-colour palettes.
|
||||
case $base_color in
|
||||
green)
|
||||
for i in {22..28} {34..40} {46..48}; do RAIN_ENGINE_COLORS+=("\e[38;5;${i}m"); done ;;
|
||||
blue)
|
||||
for i in {17..21} {27..33} {39..45}; do RAIN_ENGINE_COLORS+=("\e[38;5;${i}m"); done ;;
|
||||
red)
|
||||
for i in {52..52} {88..88} {124..124} {160..160} {196..201}; do RAIN_ENGINE_COLORS+=("\e[38;5;${i}m"); done ;;
|
||||
yellow)
|
||||
for i in {58..58} {100..100} {142..142} {184..184} {226..229}; do RAIN_ENGINE_COLORS+=("\e[38;5;${i}m"); done ;;
|
||||
cyan)
|
||||
for i in {30..31} {37..38} {44..45} {50..51}; do RAIN_ENGINE_COLORS+=("\e[38;5;${i}m"); done ;;
|
||||
*)
|
||||
RAIN_ENGINE_COLORS=("\e[37m" "\e[37;1m")
|
||||
for i in {244..255}; do RAIN_ENGINE_COLORS+=("\e[38;5;${i}m"); done ;;
|
||||
esac
|
||||
fi
|
||||
}
|
||||
|
||||
# Fill RAIN_ENGINE_CHARS with the characters a drop may be made of, according to
|
||||
# the animation ($1: rain or matrix) and the matrix charset name ($2).
|
||||
# Usage: _rain_build_chars <mode> <charset>
|
||||
_rain_build_chars()
|
||||
{
|
||||
local mode="$1"
|
||||
local charset="$2"
|
||||
RAIN_ENGINE_CHARS=()
|
||||
|
||||
case "$mode" in
|
||||
matrix)
|
||||
case "$charset" in
|
||||
""|binary)
|
||||
RAIN_ENGINE_CHARS=("0" "1")
|
||||
;;
|
||||
kana|kanji)
|
||||
# Half-width katakana set, generally rendered as single-cell glyphs.
|
||||
RAIN_ENGINE_CHARS=("ア" "イ" "ウ" "エ" "オ" "カ" "キ" "ク" "ケ" "コ" "サ" "シ" "ス" "セ" "ソ" "タ" "チ" "ツ" "テ" "ト" "ナ" "ニ" "ヌ" "ネ" "ノ" "ハ" "ヒ" "フ" "ヘ" "ホ" "マ" "ミ" "ム" "メ" "モ" "ヤ" "ユ" "ヨ" "ラ" "リ" "ル" "レ" "ロ" "ワ" "ン")
|
||||
;;
|
||||
ascii)
|
||||
RAIN_ENGINE_CHARS=("0" "1" "2" "3" "4" "5" "6" "7" "8" "9" "A" "B" "C" "D" "E" "F")
|
||||
;;
|
||||
*)
|
||||
disp E "Unknown charset: ${charset} (supported: binary, kana, ascii)."
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
;;
|
||||
*)
|
||||
RAIN_ENGINE_CHARS=("|" "│" "┃" "┆" "┇" "┊" "┋" "╽" "╿")
|
||||
;;
|
||||
esac
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
# Validate a speed and print it as a delay in seconds for read -t / sleep:
|
||||
# values >= 1 are centiseconds (5 -> 0.050), values < 1 are raw seconds.
|
||||
# Return 1 for a non-numeric or null value.
|
||||
# Usage: _rain_normalize_speed <speed>
|
||||
_rain_normalize_speed()
|
||||
{
|
||||
local raw_speed="$1"
|
||||
|
||||
# Accept integer/floating values. UI scale is centiseconds by default:
|
||||
# 5 -> 0.05s, 2.5 -> 0.025s. Values < 1 are treated as direct seconds
|
||||
# for backward compatibility (e.g. 0.03).
|
||||
if [[ ! "$raw_speed" =~ ^[0-9]+([.][0-9]+)?$ ]]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
# A null delay is refused: "read -t 0" does not wait, the animation loop
|
||||
# would spin and use a whole CPU.
|
||||
if LC_ALL=C awk -v s="$raw_speed" 'BEGIN { exit !(s == 0) }'; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
if LC_ALL=C awk -v s="$raw_speed" 'BEGIN { exit !(s < 1) }'; then
|
||||
printf "%s" "$raw_speed"
|
||||
else
|
||||
LC_ALL=C awk -v s="$raw_speed" 'BEGIN { printf "%.3f", s / 100 }'
|
||||
fi
|
||||
}
|
||||
|
||||
# Validate a density (maximum number of simultaneous drops, an integer >= 1)
|
||||
# and print it; return 1 otherwise.
|
||||
# Usage: _rain_normalize_density <density>
|
||||
_rain_normalize_density()
|
||||
{
|
||||
local raw_density="$1"
|
||||
|
||||
if [[ ! "$raw_density" =~ ^[0-9]+$ || "$raw_density" -lt 1 ]]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
printf "%s" "$raw_density"
|
||||
}
|
||||
|
||||
# Fill RAINBOW_BG_PALETTE with <width> background colour escapes sweeping the
|
||||
# hue wheel: 6 bands of 256 steps in 24-bit colour, or 12 fixed ANSI colours
|
||||
# spread over the width otherwise.
|
||||
# Usage: _rainbow_build_palette <width> <use_truecolor (0|1)>
|
||||
_rainbow_build_palette()
|
||||
{
|
||||
local palette_width="$1"
|
||||
local use_truecolor="$2"
|
||||
local x=0 wheel_pos=0 band=0 blend=0
|
||||
local red=0 green=0 blue=0
|
||||
|
||||
RAINBOW_BG_PALETTE=()
|
||||
|
||||
if (( palette_width < 1 )); then
|
||||
RAINBOW_BG_PALETTE+=("\e[41m")
|
||||
return 0
|
||||
fi
|
||||
|
||||
if (( use_truecolor )); then
|
||||
for ((x = 0; x < palette_width; x++)); do
|
||||
wheel_pos=$((x * 1536 / palette_width))
|
||||
band=$((wheel_pos / 256))
|
||||
blend=$((wheel_pos % 256))
|
||||
|
||||
case "$band" in
|
||||
0)
|
||||
red=255; green=$blend; blue=0
|
||||
;;
|
||||
1)
|
||||
red=$((255 - blend)); green=255; blue=0
|
||||
;;
|
||||
2)
|
||||
red=0; green=255; blue=$blend
|
||||
;;
|
||||
3)
|
||||
red=0; green=$((255 - blend)); blue=255
|
||||
;;
|
||||
4)
|
||||
red=$blend; green=0; blue=255
|
||||
;;
|
||||
*)
|
||||
red=255; green=0; blue=$((255 - blend))
|
||||
;;
|
||||
esac
|
||||
|
||||
RAINBOW_BG_PALETTE+=("\e[48;2;${red};${green};${blue}m")
|
||||
done
|
||||
else
|
||||
local ansi_palette=(41 101 43 103 42 102 46 106 44 104 45 105)
|
||||
local ansi_count=${#ansi_palette[@]}
|
||||
|
||||
for ((x = 0; x < palette_width; x++)); do
|
||||
RAINBOW_BG_PALETTE+=("\e[${ansi_palette[x * ansi_count / palette_width]}m")
|
||||
done
|
||||
fi
|
||||
}
|
||||
|
||||
# Stop request of the animation loops: sets exit_st, a local of the running
|
||||
# animation (dynamic scoping). Installed as the TERM/INT trap.
|
||||
_rain_stop()
|
||||
{
|
||||
exit_st=1
|
||||
}
|
||||
|
||||
# The animations read a key from the terminal and draw on it, and need tput.
|
||||
# Usage: _rain_need_tty <command-name>
|
||||
_rain_need_tty()
|
||||
{
|
||||
if [[ ! -t 0 || ! -t 1 ]]; then
|
||||
disp E "$1: a terminal is required (standard input and output must be a terminal)."
|
||||
return 1
|
||||
fi
|
||||
command -v tput >/dev/null 2>&1 || {
|
||||
disp E "The program 'tput' is required but not installed."
|
||||
return 1
|
||||
}
|
||||
}
|
||||
|
||||
# Prepare the terminal for an animation: save its settings and the TERM, INT
|
||||
# and WINCH traps of the caller, install the animation traps, disable echo,
|
||||
# hide the cursor and clear the screen.
|
||||
# saved_stty and saved_traps are locals of the calling animation.
|
||||
# Usage: _rain_enter <winch_handler_function>
|
||||
_rain_enter()
|
||||
{
|
||||
saved_stty=$(stty -g 2>/dev/null)
|
||||
saved_traps=$(trap -p TERM INT WINCH)
|
||||
trap _rain_stop TERM INT
|
||||
# shellcheck disable=SC2064 # $1 is the handler name, expanded on purpose
|
||||
trap "$1" WINCH
|
||||
stty -echo
|
||||
printf '\e[?25l\e[2J'
|
||||
}
|
||||
|
||||
# Undo _rain_enter: reset the colours (the last drawn drop colour would
|
||||
# otherwise stay active), show the cursor, clear the screen, then restore the
|
||||
# terminal settings and the caller's traps.
|
||||
_rain_leave()
|
||||
{
|
||||
printf '\e[0m\e[?25h\e[2J\e[H'
|
||||
[[ -n "$saved_stty" ]] && stty "$saved_stty"
|
||||
trap - TERM INT WINCH
|
||||
_trap_restore "$saved_traps"
|
||||
return 0
|
||||
}
|
||||
|
||||
# Shared engine of the rain and matrix animations: draws the falling drops until
|
||||
# a key is pressed, a signal is received, or the terminal is resized (the
|
||||
# state is rebuilt). The terminal state is always restored on exit.
|
||||
# Usage: _rain_engine <delay_seconds> <colour> <rain|matrix> <charset> <density>
|
||||
_rain_engine()
|
||||
{
|
||||
local step_duration="$1"
|
||||
local base_color="$2"
|
||||
local mode="$3"
|
||||
local charset="$4"
|
||||
local density_override="$5"
|
||||
|
||||
_rain_need_tty "$mode" || return 1
|
||||
|
||||
_rain_build_colors "$base_color" || return 1
|
||||
_rain_build_chars "$mode" "$charset" || return 1
|
||||
|
||||
local rain_colors=("${RAIN_ENGINE_COLORS[@]}")
|
||||
local rain_chars=("${RAIN_ENGINE_CHARS[@]}")
|
||||
unset RAIN_ENGINE_COLORS RAIN_ENGINE_CHARS
|
||||
local rain_color_tab=${#rain_colors[@]}
|
||||
local rain_tab=${#rain_chars[@]}
|
||||
local matrix_head_color=$'\e[1;97m'
|
||||
|
||||
local exit_st=0
|
||||
local num_rain_metadata=5
|
||||
local term_height=0 term_width=0
|
||||
local X=0 Y=0 drop_length=0 rain_drop=0
|
||||
local max_rain_width=0 max_rain_height=0
|
||||
local new_rain_odd=0 falling_odd=0
|
||||
local term_area=0
|
||||
local frame_sleep="$step_duration"
|
||||
local saved_stty="" saved_traps=""
|
||||
|
||||
# The drops: 5 consecutive entries each (X, Y of the top, character, colour,
|
||||
# length).
|
||||
local rains=()
|
||||
local num_rains=0
|
||||
local ch=""
|
||||
|
||||
# WINCH handler (and initial sizing). Called with the animation running, it
|
||||
# sees its locals through dynamic scoping.
|
||||
_rain_sigwinch()
|
||||
{
|
||||
term_width=$(tput cols)
|
||||
term_height=$(tput lines)
|
||||
term_area=$(( term_width * term_height ))
|
||||
|
||||
case "$mode" in
|
||||
matrix)
|
||||
max_rain_width=$(( term_area / 3 ))
|
||||
max_rain_height=$(( term_height < 8 ? 1 : term_height / 6 ))
|
||||
new_rain_odd=$(( term_height > 50 ? 100 : term_height * 2 ))
|
||||
new_rain_odd=$(( new_rain_odd * 85 / 100 ))
|
||||
falling_odd=100
|
||||
|
||||
# Adapt cadence and density to terminal size for smoother rendering.
|
||||
if ((term_area < 1200)); then
|
||||
max_rain_width=$(( term_area / 4 ))
|
||||
frame_sleep=$(LC_ALL=C awk -v s="$step_duration" 'BEGIN { printf "%.3f", s * 1.15 }')
|
||||
elif ((term_area > 5000)); then
|
||||
max_rain_width=$(( term_area / 2 ))
|
||||
frame_sleep=$(LC_ALL=C awk -v s="$step_duration" 'BEGIN { printf "%.3f", s * 0.85 }')
|
||||
else
|
||||
frame_sleep="$step_duration"
|
||||
fi
|
||||
;;
|
||||
*)
|
||||
max_rain_width=$(( term_area / 4 ))
|
||||
max_rain_height=$(( term_height < 10 ? 1 : term_height / 10 ))
|
||||
new_rain_odd=$(( term_height > 50 ? 100 : term_height * 2 ))
|
||||
new_rain_odd=$(( new_rain_odd * 75 / 100 ))
|
||||
falling_odd=$(( term_height > 25 ? 100 : term_height * 4 ))
|
||||
falling_odd=$(( falling_odd * 90 / 100 ))
|
||||
frame_sleep="$step_duration"
|
||||
;;
|
||||
esac
|
||||
|
||||
if [[ -n "$density_override" ]]; then
|
||||
max_rain_width="$density_override"
|
||||
fi
|
||||
|
||||
# The drops were placed for the previous size and the screen content
|
||||
# does not fit the new one: start again from an empty screen.
|
||||
rains=()
|
||||
num_rains=0
|
||||
printf '\e[2J'
|
||||
}
|
||||
|
||||
# Draw one frame. The whole frame (erasing the previous drop positions,
|
||||
# then the new ones) is built in a string and written with one printf: a
|
||||
# write per character made the animation slow and flickering.
|
||||
_rain_render()
|
||||
{
|
||||
local idx=0 y=0 drop_color="" current_char="" render_color="" out=""
|
||||
local esc=$'\e'
|
||||
|
||||
for ((idx = 0; idx < num_rains * num_rain_metadata; idx += num_rain_metadata)); do
|
||||
X=${rains[idx]}
|
||||
Y=${rains[idx + 1]}
|
||||
drop_length=${rains[idx + 4]}
|
||||
for ((y = Y; y < Y + drop_length; y++)); do
|
||||
((y < 1 || y > term_height)) && continue
|
||||
out+="${esc}[${y};${X}H "
|
||||
done
|
||||
done
|
||||
|
||||
for ((idx = 0; idx < num_rains * num_rain_metadata; idx += num_rain_metadata)); do
|
||||
if ((100 * RANDOM / 32768 < falling_odd)); then
|
||||
if ((++rains[idx + 1] > term_height)); then
|
||||
# The drop left the screen: remove it. The next drop moves
|
||||
# into this slot, which must be processed again (the loop
|
||||
# increment would skip it, and it would vanish for a frame).
|
||||
rains=("${rains[@]:0:idx}" "${rains[@]:idx+num_rain_metadata}")
|
||||
num_rains=$(( num_rains - 1 ))
|
||||
idx=$(( idx - num_rain_metadata ))
|
||||
continue
|
||||
fi
|
||||
fi
|
||||
|
||||
X=${rains[idx]}
|
||||
Y=${rains[idx + 1]}
|
||||
rain_drop=${rains[idx + 2]}
|
||||
drop_color=${rains[idx + 3]}
|
||||
drop_length=${rains[idx + 4]}
|
||||
|
||||
for ((y = Y; y < Y + drop_length; y++)); do
|
||||
((y < 1 || y > term_height)) && continue
|
||||
if [[ "$mode" == "matrix" ]]; then
|
||||
current_char="${rain_chars[rain_tab * RANDOM / 32768]}"
|
||||
if ((y == Y + drop_length - 1)); then
|
||||
render_color="$matrix_head_color"
|
||||
else
|
||||
render_color="$drop_color"
|
||||
fi
|
||||
else
|
||||
current_char="$rain_drop"
|
||||
render_color="$drop_color"
|
||||
fi
|
||||
# render_color holds a literal \e[...m: hence %b below
|
||||
out+="${esc}[${y};${X}H${render_color}${current_char}"
|
||||
done
|
||||
done
|
||||
|
||||
[[ -n "$out" ]] && printf '%b' "$out"
|
||||
return 0
|
||||
}
|
||||
|
||||
_rain_enter _rain_sigwinch
|
||||
_rain_sigwinch
|
||||
while ((exit_st <= 0)); do
|
||||
ch=""
|
||||
# A timeout makes read fail (status > 128): expected, not an error
|
||||
read -r -n 1 -t "$frame_sleep" ch || true
|
||||
case "$ch" in
|
||||
q|Q)
|
||||
_rain_stop
|
||||
;;
|
||||
esac
|
||||
|
||||
if ((num_rains < max_rain_width)) && ((100 * RANDOM / 32768 < new_rain_odd)); then
|
||||
rain_drop="${rain_chars[rain_tab * RANDOM / 32768]}"
|
||||
drop_color="${rain_colors[rain_color_tab * RANDOM / 32768]}"
|
||||
if [[ "$mode" == "matrix" ]]; then
|
||||
drop_length=$((max_rain_height * RANDOM / 32768 + 2))
|
||||
else
|
||||
drop_length=$((max_rain_height * RANDOM / 32768 + 1))
|
||||
fi
|
||||
X=$((term_width * RANDOM / 32768 + 1))
|
||||
Y=$((1 - drop_length))
|
||||
rains+=("$X" "$Y" "$rain_drop" "$drop_color" "$drop_length")
|
||||
num_rains=$(( num_rains + 1 ))
|
||||
fi
|
||||
|
||||
_rain_render
|
||||
done
|
||||
|
||||
_rain_leave
|
||||
unset -f _rain_sigwinch _rain_render
|
||||
return 0
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
#!/usr/bin/env bash
|
||||
# ------------------------------------------------------------------------------
|
||||
# Copyright (c) 2013-2026 Geoffray Levasseur <fatalerrors@geoffray-levasseur.org>
|
||||
# Protected by the BSD3 license. Please read bellow for details.
|
||||
#
|
||||
# * Redistribution and use in source and binary forms,
|
||||
# * with or without modification, are permitted provided
|
||||
# * that the following conditions are met:
|
||||
# *
|
||||
# * Redistributions of source code must retain the above
|
||||
# * copyright notice, this list of conditions and the
|
||||
# * following disclaimer.
|
||||
# *
|
||||
# * Redistributions in binary form must reproduce the above
|
||||
# * copyright notice, this list of conditions and the following
|
||||
# * disclaimer in the documentation and/or other materials
|
||||
# * provided with the distribution.
|
||||
# *
|
||||
# * Neither the name of the copyright holder nor the names
|
||||
# * of any other contributors may be used to endorse or
|
||||
# * promote products derived from this software without
|
||||
# * specific prior written permission.
|
||||
# *
|
||||
# * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
|
||||
# * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
|
||||
# * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
|
||||
# * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
# * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
|
||||
# * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
# * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
|
||||
# * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
# * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||
# * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
|
||||
# * OF SUCH DAMAGE.
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
# Private helpers of profile.d/ssh.sh, loaded by the module itself.
|
||||
|
||||
# Internal helper: remove <key> (a name or an IP, in known_hosts notation, so
|
||||
# also "[host]:port") from the known_hosts file <file> with ssh-keygen -R.
|
||||
# ssh-keygen writes a new file: run by root on another user's file (--all-users)
|
||||
# it would leave it owned by root with mode 0644, which keeps that user's ssh
|
||||
# from adding host keys, or exposes a private file. The owner and mode of the
|
||||
# file, and of its ".old" backup, are therefore put back.
|
||||
# Returns ssh-keygen's status (non-zero when the key was not found).
|
||||
# Usage: _rmhost_remove <key> <file>
|
||||
_rmhost_remove()
|
||||
{
|
||||
local key="$1" file="$2" owner="" mode="" rc=0
|
||||
if [[ ${EUID:-$(id -u)} -eq 0 ]]; then
|
||||
owner=$(stat -c '%u:%g' -- "$file" 2>/dev/null)
|
||||
mode=$(stat -c '%a' -- "$file" 2>/dev/null)
|
||||
fi
|
||||
|
||||
ssh-keygen -R "$key" -f "$file" </dev/null >/dev/null 2>&1 || rc=$?
|
||||
|
||||
if [[ -n $owner && -n $mode ]]; then
|
||||
chown "$owner" -- "$file" "${file}.old" 2>/dev/null
|
||||
chmod "$mode" -- "$file" "${file}.old" 2>/dev/null
|
||||
fi
|
||||
return "$rc"
|
||||
}
|
||||
export -f _rmhost_remove
|
||||
@@ -0,0 +1,62 @@
|
||||
#!/usr/bin/env bash
|
||||
# ------------------------------------------------------------------------------
|
||||
# Copyright (c) 2013-2026 Geoffray Levasseur <fatalerrors@geoffray-levasseur.org>
|
||||
# Protected by the BSD3 license. Please read bellow for details.
|
||||
#
|
||||
# * Redistribution and use in source and binary forms,
|
||||
# * with or without modification, are permitted provided
|
||||
# * that the following conditions are met:
|
||||
# *
|
||||
# * Redistributions of source code must retain the above
|
||||
# * copyright notice, this list of conditions and the
|
||||
# * following disclaimer.
|
||||
# *
|
||||
# * Redistributions in binary form must reproduce the above
|
||||
# * copyright notice, this list of conditions and the following
|
||||
# * disclaimer in the documentation and/or other materials
|
||||
# * provided with the distribution.
|
||||
# *
|
||||
# * Neither the name of the copyright holder nor the names
|
||||
# * of any other contributors may be used to endorse or
|
||||
# * promote products derived from this software without
|
||||
# * specific prior written permission.
|
||||
# *
|
||||
# * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
|
||||
# * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
|
||||
# * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
|
||||
# * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
# * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
|
||||
# * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
# * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
|
||||
# * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
# * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||
# * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
|
||||
# * OF SUCH DAMAGE.
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
# Private helpers of profile.d/updates.sh, loaded by the module itself.
|
||||
|
||||
# Check a branch name. It ends up in URLs and in git command lines, so it is
|
||||
# restricted to plain ref characters, without a leading "-" or "..".
|
||||
# Usage: _updt_valid_branch <name>
|
||||
_updt_valid_branch()
|
||||
{
|
||||
[[ "$1" =~ ^[A-Za-z0-9][A-Za-z0-9._/-]*$ && "$1" != *..* ]]
|
||||
}
|
||||
|
||||
# Tell whether the published version $2 is an update for the installed one $1.
|
||||
# Versions look like MAJOR.MINOR.PATCH[-REVISION[_STAGE_N]]. The base version
|
||||
# (before the first "-" or "_") decides, compared in version order; with the same
|
||||
# base, any difference (a new revision, a release candidate becoming final) is
|
||||
# an update. Returns 0 when it is, 1 otherwise (equal, or the installed version
|
||||
# is the more recent one, as on a development branch).
|
||||
# Usage: _updt_is_newer <installed> <published>
|
||||
_updt_is_newer()
|
||||
{
|
||||
local cur="$1" new="$2" cur_base new_base
|
||||
[[ "$cur" == "$new" ]] && return 1
|
||||
cur_base="${cur%%[-_]*}"
|
||||
new_base="${new%%[-_]*}"
|
||||
[[ "$cur_base" == "$new_base" ]] && return 0
|
||||
[[ "$(printf '%s\n%s\n' "$cur_base" "$new_base" | sort -V | tail -n 1)" == "$new_base" ]]
|
||||
}
|
||||
@@ -0,0 +1,253 @@
|
||||
#!/usr/bin/env bash
|
||||
# ------------------------------------------------------------------------------
|
||||
# Copyright (c) 2013-2026 Geoffray Levasseur <fatalerrors@geoffray-levasseur.org>
|
||||
# Protected by the BSD3 license. Please read bellow for details.
|
||||
#
|
||||
# * Redistribution and use in source and binary forms,
|
||||
# * with or without modification, are permitted provided
|
||||
# * that the following conditions are met:
|
||||
# *
|
||||
# * Redistributions of source code must retain the above
|
||||
# * copyright notice, this list of conditions and the
|
||||
# * following disclaimer.
|
||||
# *
|
||||
# * Redistributions in binary form must reproduce the above
|
||||
# * copyright notice, this list of conditions and the following
|
||||
# * disclaimer in the documentation and/or other materials
|
||||
# * provided with the distribution.
|
||||
# *
|
||||
# * Neither the name of the copyright holder nor the names
|
||||
# * of any other contributors may be used to endorse or
|
||||
# * promote products derived from this software without
|
||||
# * specific prior written permission.
|
||||
# *
|
||||
# * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
|
||||
# * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
|
||||
# * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
|
||||
# * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
# * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
|
||||
# * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
# * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
|
||||
# * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
# * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||
# * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
|
||||
# * OF SUCH DAMAGE.
|
||||
# ------------------------------------------------------------------------------
|
||||
# Git helper completions for profile.d/git.sh shortcuts.
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
# Return 0 when current directory is inside a git work tree.
|
||||
_profile_git_in_repo()
|
||||
{
|
||||
git rev-parse --is-inside-work-tree >/dev/null 2>&1
|
||||
}
|
||||
|
||||
# Load git completion helpers on demand if they are available on the system.
|
||||
_profile_git_load_completion_helpers()
|
||||
{
|
||||
declare -F __git_complete >/dev/null 2>&1 && return 0
|
||||
|
||||
local completion_file
|
||||
for completion_file in \
|
||||
/usr/share/bash-completion/completions/git \
|
||||
/usr/share/git/completion/git-completion.bash \
|
||||
/etc/bash_completion.d/git
|
||||
do
|
||||
if [[ -r "$completion_file" ]]; then
|
||||
# shellcheck source=/dev/null
|
||||
. "$completion_file"
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
declare -F __git_complete >/dev/null 2>&1
|
||||
}
|
||||
|
||||
_profile_git_complete_remotes()
|
||||
{
|
||||
local cur
|
||||
cur="${COMP_WORDS[COMP_CWORD]}"
|
||||
|
||||
if ! _profile_git_in_repo; then
|
||||
COMPREPLY=()
|
||||
return 0
|
||||
fi
|
||||
|
||||
mapfile -t COMPREPLY < <(compgen -W "$(git remote 2>/dev/null)" -- "$cur")
|
||||
}
|
||||
|
||||
_profile_git_complete_refs()
|
||||
{
|
||||
local cur
|
||||
cur="${COMP_WORDS[COMP_CWORD]}"
|
||||
|
||||
if ! _profile_git_in_repo; then
|
||||
COMPREPLY=()
|
||||
return 0
|
||||
fi
|
||||
|
||||
mapfile -t COMPREPLY < <(compgen -W "$(git for-each-ref --format='%(refname:short)' refs/heads refs/remotes refs/tags 2>/dev/null)" -- "$cur")
|
||||
}
|
||||
|
||||
_profile_git_complete_add_paths()
|
||||
{
|
||||
# shellcheck disable=SC2034 # Used indirectly by git-completion helpers via dynamic scope.
|
||||
local cur words cword prev __git_cmd_idx=0
|
||||
local complete_opt="--others --modified --directory --no-empty-directory"
|
||||
|
||||
if declare -F __git_complete_index_file >/dev/null 2>&1; then
|
||||
if declare -F _get_comp_words_by_ref >/dev/null 2>&1; then
|
||||
_get_comp_words_by_ref -n =: cur words cword prev
|
||||
else
|
||||
cur="${COMP_WORDS[COMP_CWORD]}"
|
||||
if (( COMP_CWORD > 0 )); then
|
||||
prev="${COMP_WORDS[COMP_CWORD-1]}"
|
||||
else
|
||||
prev=""
|
||||
fi
|
||||
# shellcheck disable=SC2034 # Used indirectly by git-completion helpers via dynamic scope.
|
||||
cword="$COMP_CWORD"
|
||||
# shellcheck disable=SC2034 # Used indirectly by git-completion helpers via dynamic scope.
|
||||
words=("${COMP_WORDS[@]}")
|
||||
fi
|
||||
|
||||
if [[ -n $(__git_find_on_cmdline "-u --update") ]]; then
|
||||
complete_opt="--modified"
|
||||
fi
|
||||
__git_complete_index_file "$complete_opt"
|
||||
return 0
|
||||
fi
|
||||
|
||||
mapfile -t COMPREPLY < <(compgen -f -- "${COMP_WORDS[COMP_CWORD]}")
|
||||
}
|
||||
|
||||
_complete_gst()
|
||||
{
|
||||
local cur
|
||||
cur="${COMP_WORDS[COMP_CWORD]}"
|
||||
|
||||
case "$cur" in
|
||||
-*)
|
||||
mapfile -t COMPREPLY < <(compgen -W "-h --help" -- "$cur")
|
||||
;;
|
||||
*)
|
||||
mapfile -t COMPREPLY < <(compgen -d -- "$cur")
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
_complete_ggraph()
|
||||
{
|
||||
local cur prev
|
||||
cur="${COMP_WORDS[COMP_CWORD]}"
|
||||
prev="${COMP_WORDS[COMP_CWORD-1]}"
|
||||
|
||||
case "$prev" in
|
||||
-n|--limit)
|
||||
COMPREPLY=()
|
||||
return 0
|
||||
;;
|
||||
esac
|
||||
|
||||
mapfile -t COMPREPLY < <(compgen -W "-h --help -n --limit" -- "$cur")
|
||||
}
|
||||
|
||||
_complete_gsync()
|
||||
{
|
||||
local cur
|
||||
cur="${COMP_WORDS[COMP_CWORD]}"
|
||||
|
||||
if [[ $cur == -* ]]; then
|
||||
mapfile -t COMPREPLY < <(compgen -W "-h --help" -- "$cur")
|
||||
return 0
|
||||
fi
|
||||
|
||||
_profile_git_complete_remotes
|
||||
}
|
||||
|
||||
_complete_gacp()
|
||||
{
|
||||
local cur prev
|
||||
cur="${COMP_WORDS[COMP_CWORD]}"
|
||||
prev="${COMP_WORDS[COMP_CWORD-1]}"
|
||||
|
||||
case "$prev" in
|
||||
-m|--message)
|
||||
COMPREPLY=()
|
||||
return 0
|
||||
;;
|
||||
esac
|
||||
|
||||
if [[ $cur == -* ]]; then
|
||||
mapfile -t COMPREPLY < <(compgen -W "-h --help -a --auto -m --message" -- "$cur")
|
||||
return 0
|
||||
fi
|
||||
|
||||
_profile_git_complete_add_paths
|
||||
}
|
||||
|
||||
_complete_greset()
|
||||
{
|
||||
local cur
|
||||
cur="${COMP_WORDS[COMP_CWORD]}"
|
||||
|
||||
if [[ $cur == -* ]]; then
|
||||
mapfile -t COMPREPLY < <(compgen -W "-h --help -x --with-ignored" -- "$cur")
|
||||
return 0
|
||||
fi
|
||||
|
||||
_profile_git_complete_refs
|
||||
}
|
||||
|
||||
_complete_gwip()
|
||||
{
|
||||
local cur
|
||||
cur="${COMP_WORDS[COMP_CWORD]}"
|
||||
|
||||
if [[ $cur == -* ]]; then
|
||||
mapfile -t COMPREPLY < <(compgen -W "-h --help" -- "$cur")
|
||||
else
|
||||
COMPREPLY=()
|
||||
fi
|
||||
}
|
||||
|
||||
_complete_gprune()
|
||||
{
|
||||
local cur
|
||||
cur="${COMP_WORDS[COMP_CWORD]}"
|
||||
|
||||
if [[ $cur == -* ]]; then
|
||||
mapfile -t COMPREPLY < <(compgen -W "-h --help" -- "$cur")
|
||||
return 0
|
||||
fi
|
||||
|
||||
_profile_git_complete_refs
|
||||
}
|
||||
|
||||
_complete_groot()
|
||||
{
|
||||
local cur
|
||||
cur="${COMP_WORDS[COMP_CWORD]}"
|
||||
|
||||
mapfile -t COMPREPLY < <(compgen -W "-h --help -g --go" -- "$cur")
|
||||
}
|
||||
|
||||
_profile_git_register_completions()
|
||||
{
|
||||
complete -F _complete_gst gst
|
||||
complete -F _complete_ggraph ggraph
|
||||
complete -F _complete_gsync gsync
|
||||
complete -F _complete_gacp gacp
|
||||
complete -F _complete_greset greset
|
||||
complete -F _complete_gwip gwip
|
||||
complete -F _complete_gprune gprune
|
||||
complete -F _complete_groot groot
|
||||
}
|
||||
|
||||
# Register completions only in interactive bash sessions.
|
||||
if [[ $- == *i* && -n ${BASH_VERSION:-} ]]; then
|
||||
_profile_git_load_completion_helpers >/dev/null 2>&1 || true
|
||||
_profile_git_register_completions
|
||||
fi
|
||||
|
||||
# EOF
|
||||
@@ -0,0 +1,87 @@
|
||||
#!/usr/bin/env bash
|
||||
# ------------------------------------------------------------------------------
|
||||
# Copyright (c) 2013-2026 Geoffray Levasseur <fatalerrors@geoffray-levasseur.org>
|
||||
# Protected by the BSD3 license. Please read bellow for details.
|
||||
#
|
||||
# * Redistribution and use in source and binary forms,
|
||||
# * with or without modification, are permitted provided
|
||||
# * that the following conditions are met:
|
||||
# *
|
||||
# * Redistributions of source code must retain the above
|
||||
# * copyright notice, this list of conditions and the
|
||||
# * following disclaimer.
|
||||
# *
|
||||
# * Redistributions in binary form must reproduce the above
|
||||
# * copyright notice, this list of conditions and the following
|
||||
# * disclaimer in the documentation and/or other materials
|
||||
# * provided with the distribution.
|
||||
# *
|
||||
# * Neither the name of the copyright holder nor the names
|
||||
# * of any other contributors may be used to endorse or
|
||||
# * promote products derived from this software without
|
||||
# * specific prior written permission.
|
||||
# *
|
||||
# * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
|
||||
# * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
|
||||
# * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
|
||||
# * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
# * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
|
||||
# * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
# * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
|
||||
# * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
# * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||
# * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
|
||||
# * OF SUCH DAMAGE.
|
||||
# ------------------------------------------------------------------------------
|
||||
# Prompt helper completions for profile.d/prompt.sh shortcuts.
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
_profile_prompt_complete_theme_names()
|
||||
{
|
||||
local theme_dir="${PROMPT_THEME_DIR:-${MYPATH}/profile.d/themes}"
|
||||
|
||||
[[ -d "$theme_dir" ]] || return 0
|
||||
|
||||
local theme_file theme_names=""
|
||||
for theme_file in "$theme_dir"/*.theme; do
|
||||
[[ -f "$theme_file" ]] || continue
|
||||
theme_names+=" ${theme_file##*/}"
|
||||
theme_names="${theme_names%.theme}"
|
||||
done
|
||||
|
||||
printf "%s\n" "$theme_names"
|
||||
}
|
||||
|
||||
_complete_set_theme()
|
||||
{
|
||||
local cur prev
|
||||
cur="${COMP_WORDS[COMP_CWORD]}"
|
||||
prev="${COMP_WORDS[COMP_CWORD-1]}"
|
||||
|
||||
case "$prev" in
|
||||
-h|--help|-l|--list)
|
||||
COMPREPLY=()
|
||||
return 0
|
||||
;;
|
||||
-p|--preview|-S|--save)
|
||||
;;
|
||||
esac
|
||||
|
||||
if [[ "$cur" == -* ]]; then
|
||||
mapfile -t COMPREPLY < <(compgen -W "-h --help -l --list -p --preview -S --save" -- "$cur")
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [[ "$cur" == */* || "$cur" == .* ]]; then
|
||||
mapfile -t COMPREPLY < <(compgen -f -X '!*.theme' -- "$cur")
|
||||
return 0
|
||||
fi
|
||||
|
||||
mapfile -t COMPREPLY < <(compgen -W "$(_profile_prompt_complete_theme_names)" -- "$cur")
|
||||
}
|
||||
|
||||
if [[ $- == *i* && -n ${BASH_VERSION:-} ]]; then
|
||||
complete -F _complete_set_theme set_theme
|
||||
fi
|
||||
|
||||
# EOF
|
||||
+910
-191
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,512 @@
|
||||
#!/usr/bin/env bash
|
||||
# ------------------------------------------------------------------------------
|
||||
# Copyright (c) 2013-2026 Geoffray Levasseur <fatalerrors@geoffray-levasseur.org>
|
||||
# Protected by the BSD3 license. Please read bellow for details.
|
||||
#
|
||||
# * Redistribution and use in source and binary forms,
|
||||
# * with or without modification, are permitted provided
|
||||
# * that the following conditions are met:
|
||||
# *
|
||||
# * Redistributions of source code must retain the above
|
||||
# * copyright notice, this list of conditions and the
|
||||
# * following disclaimer.
|
||||
# *
|
||||
# * Redistributions in binary form must reproduce the above
|
||||
# * copyright notice, this list of conditions and the following
|
||||
# * disclaimer in the documentation and/or other materials
|
||||
# * provided with the distribution.
|
||||
# *
|
||||
# * Neither the name of the copyright holder nor the names
|
||||
# * of any other contributors may be used to endorse or
|
||||
# * promote products derived from this software without
|
||||
# * specific prior written permission.
|
||||
# *
|
||||
# * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
|
||||
# * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
|
||||
# * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
|
||||
# * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
# * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
|
||||
# * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
# * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
|
||||
# * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
# * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||
# * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
|
||||
# * OF SUCH DAMAGE.
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
# Common helpers, then the ones private to this module (helpers/conf.sh)
|
||||
# shellcheck source=../helpers/common.sh
|
||||
declare -F _profile_load_helpers >/dev/null || . "$(dirname -- "${BASH_SOURCE[0]}")/../helpers/common.sh"
|
||||
_profile_load_helpers "${BASH_SOURCE[0]}"
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# Save or update a key=value pair in a section of the profile configuration file.
|
||||
# The user configuration ($HOME/.profile.conf) is updated when it exists,
|
||||
# otherwise the installation configuration ($PROFILE_CONF or $MYPATH/profile.conf)
|
||||
# is used. The section header is created automatically when absent.
|
||||
#
|
||||
# An existing key is replaced in place (duplicates in the same section are
|
||||
# dropped); a new key is added at the end of the section's active lines, before
|
||||
# its trailing blank/comment lines, so it never ends up under the banner of the
|
||||
# next section. The running shell is NOT updated: the value is read again by
|
||||
# parse_conf/load_conf the next time the profile is sourced.
|
||||
#
|
||||
# The value is stored verbatim, but parse_conf trims surrounding whitespace and
|
||||
# quotes, drops backticks and "$(", and expands "$VAR" references when it reads
|
||||
# the file back; values relying on those are therefore not preserved.
|
||||
#
|
||||
# Usage: conf_save <section> <key> <value>
|
||||
# section : INI section name without brackets, e.g. "prompt" for [prompt]
|
||||
# key : variable name to set (alphanumeric and underscore only)
|
||||
# value : value to assign (may be empty, must be a single line)
|
||||
conf_save()
|
||||
{
|
||||
if [[ $# -ne 3 ]]; then
|
||||
disp E "Usage: conf_save <section> <key> <value>"
|
||||
return 1
|
||||
fi
|
||||
|
||||
local section="$1" key="$2" value="$3"
|
||||
|
||||
if ! [[ "$section" =~ ^[a-zA-Z0-9_]+$ ]]; then
|
||||
disp E "conf_save: invalid section name '${section}' (alphanumeric and underscore only)."
|
||||
return 1
|
||||
fi
|
||||
if ! [[ "$key" =~ ^[a-zA-Z_][a-zA-Z0-9_]*$ ]]; then
|
||||
disp E "conf_save: invalid key name '${key}' (alphanumeric and underscore only)."
|
||||
return 1
|
||||
fi
|
||||
# A line break would split the entry and corrupt the file.
|
||||
if [[ "$value" == *$'\n'* || "$value" == *$'\r'* ]]; then
|
||||
disp E "conf_save: the value must not contain line breaks."
|
||||
return 1
|
||||
fi
|
||||
|
||||
local conf_file
|
||||
conf_file=$(_conf_path)
|
||||
# Follow a symlink (e.g. dotfiles manager) so the link itself is not
|
||||
# replaced by a regular file when the temporary file is moved over it.
|
||||
[[ -L "$conf_file" ]] && conf_file=$(readlink -f -- "$conf_file")
|
||||
|
||||
# Directory part of the path; a bare file name lives in the current directory.
|
||||
local conf_dir=.
|
||||
[[ "$conf_file" == */* ]] && conf_dir="${conf_file%/*}"
|
||||
[[ -d "$conf_dir" ]] || mkdir -p "$conf_dir" || {
|
||||
disp E "conf_save: unable to create configuration directory: ${conf_dir}"
|
||||
return 1
|
||||
}
|
||||
|
||||
if [[ ! -e "$conf_file" ]]; then
|
||||
{
|
||||
printf "[%s]\n" "$section"
|
||||
printf "%s=%s\n" "$key" "$value"
|
||||
} > "$conf_file" || {
|
||||
disp E "conf_save: unable to write configuration file: ${conf_file}"
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Temporary file next to the target so the final mv is an atomic rename.
|
||||
local tmp_file="${conf_file}.tmp.$$"
|
||||
# The value is passed via ENVIRON (not awk -v) so backslashes are preserved
|
||||
# verbatim; awk -v would interpret C escape sequences (\t, \n, \e, ...).
|
||||
# awk state machine: in_sec is true while inside the target section, wrote
|
||||
# once the key has been emitted, saw_sec once the section has been seen.
|
||||
_CONF_SAVE_VAL="$value" \
|
||||
awk -v sec="$section" -v key="$key" '
|
||||
# Emit the held trailing blank/comment lines
|
||||
function flush_pending( i) {
|
||||
for (i = 1; i <= np; i++) print pend[i]
|
||||
np = 0
|
||||
}
|
||||
|
||||
BEGIN {
|
||||
val = ENVIRON["_CONF_SAVE_VAL"]
|
||||
in_sec = 0
|
||||
saw_sec = 0
|
||||
wrote = 0
|
||||
np = 0
|
||||
}
|
||||
|
||||
{
|
||||
# Section header (leading blanks allowed, as parse_conf does)
|
||||
if ($0 ~ /^[[:space:]]*\[[^]]+\][[:space:]]*$/) {
|
||||
# Leaving the target section without having found the key:
|
||||
# add it now, before the section trailing comments.
|
||||
if (in_sec && !wrote) {
|
||||
print key "=" val
|
||||
wrote = 1
|
||||
}
|
||||
flush_pending()
|
||||
|
||||
if ($0 ~ ("^[[:space:]]*\\[" sec "\\][[:space:]]*$")) {
|
||||
in_sec = 1
|
||||
saw_sec = 1
|
||||
} else {
|
||||
in_sec = 0
|
||||
}
|
||||
|
||||
print
|
||||
next
|
||||
}
|
||||
|
||||
# Until the key is written, hold blank and comment lines: if the
|
||||
# section ends without the key, it must go before them.
|
||||
if (in_sec && !wrote && $0 ~ /^[[:space:]]*([#;].*)?$/) {
|
||||
pend[++np] = $0
|
||||
next
|
||||
}
|
||||
flush_pending()
|
||||
|
||||
# Existing assignment of the key: replace the first, drop the others
|
||||
if (in_sec && $0 ~ ("^[[:space:]]*" key "[[:space:]]*=")) {
|
||||
if (!wrote) {
|
||||
print key "=" val
|
||||
wrote = 1
|
||||
}
|
||||
next
|
||||
}
|
||||
|
||||
print
|
||||
}
|
||||
|
||||
END {
|
||||
if (in_sec && !wrote) {
|
||||
print key "=" val
|
||||
wrote = 1
|
||||
}
|
||||
flush_pending()
|
||||
if (!saw_sec) {
|
||||
if (NR > 0) print ""
|
||||
print "[" sec "]"
|
||||
print key "=" val
|
||||
}
|
||||
}
|
||||
' "$conf_file" > "$tmp_file" || {
|
||||
rm -f "$tmp_file"
|
||||
disp E "conf_save: unable to update configuration file: ${conf_file}"
|
||||
return 1
|
||||
}
|
||||
|
||||
# mktemp-style default permissions must not replace the original ones
|
||||
# (the file may hold private values); best effort, GNU chmod only.
|
||||
chmod --reference="$conf_file" -- "$tmp_file" 2>/dev/null
|
||||
|
||||
mv -- "$tmp_file" "$conf_file" || {
|
||||
rm -f "$tmp_file"
|
||||
disp E "conf_save: unable to replace configuration file: ${conf_file}"
|
||||
return 1
|
||||
}
|
||||
}
|
||||
export -f conf_save
|
||||
# ------------------------------------------------------------------------------
|
||||
# Display the profile configuration file, with optional section and key filters.
|
||||
# The same file resolution as conf_save is used: $HOME/.profile.conf when
|
||||
# present, otherwise $PROFILE_CONF or $MYPATH/profile.conf.
|
||||
#
|
||||
# Usage: conf_dump [options] [pattern]
|
||||
# -s, --section NAME : Only display the given section
|
||||
# -a, --all : Also show commented-out keys (default values, in white)
|
||||
# pattern : Only display keys whose name contains this substring
|
||||
#
|
||||
# Output colours:
|
||||
# green — key is explicitly set (uncommented) in the config file
|
||||
# white — key is present but commented out (shows the default value)
|
||||
conf_dump()
|
||||
{
|
||||
local section="" key_pattern="" show_all=0
|
||||
|
||||
local PARSED
|
||||
PARSED=$(getopt -o hs:a --long help,section:,all -n 'conf_dump' -- "$@")
|
||||
# shellcheck disable=SC2181
|
||||
if [[ $? -ne 0 ]]; then
|
||||
disp E "Invalid options, use \"conf_dump --help\" to display usage."
|
||||
return 1
|
||||
fi
|
||||
# Restore the getopt-normalised (shell-quoted) argument list, "--" ends options
|
||||
local -a _pargs=()
|
||||
_getopt_split _pargs "$PARSED"
|
||||
set -- "${_pargs[@]}"
|
||||
|
||||
while true; do
|
||||
case "$1" in
|
||||
-h|--help)
|
||||
printf "conf_dump: Display the profile configuration file, with optional filters.\n\n"
|
||||
printf "Usage: conf_dump [options] [pattern]\n\n"
|
||||
printf "Options:\n"
|
||||
printf "\t-h, --help\t\tDisplay this help screen\n"
|
||||
printf "\t-s, --section NAME\tOnly display the given section\n"
|
||||
printf "\t-a, --all\t\tAlso show commented-out keys (default values)\n\n"
|
||||
printf "Arguments:\n"
|
||||
printf "\tpattern\tOnly display keys whose name contains this substring\n\n"
|
||||
printf "Output colours:\n"
|
||||
printf "\t\033[1;32mgreen\033[0m — key is explicitly set (active)\n"
|
||||
printf "\t\033[1;97mwhite\033[0m — key is commented out (default value)\n"
|
||||
return 0
|
||||
;;
|
||||
-s|--section)
|
||||
section="$2"
|
||||
shift 2
|
||||
;;
|
||||
-a|--all)
|
||||
show_all=1
|
||||
shift
|
||||
;;
|
||||
--)
|
||||
shift
|
||||
break
|
||||
;;
|
||||
*)
|
||||
disp E "Invalid option, use \"conf_dump --help\" to display options list"
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
[[ $# -gt 0 ]] && key_pattern="$1"
|
||||
|
||||
local conf_file
|
||||
conf_file=$(_conf_path)
|
||||
|
||||
if [[ ! -f "$conf_file" ]]; then
|
||||
disp E "conf_dump: configuration file not found: ${conf_file}"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Honour NO_COLOR like disp does: empty colour strings give plain output.
|
||||
local c_sec="${Blue:-}" c_active="${BGreen:-}" c_default="${BIWhite:-}" c_rst="${RESETCOL:-}"
|
||||
if [[ -n ${NO_COLOR:-} ]]; then
|
||||
c_sec="" c_active="" c_default="" c_rst=""
|
||||
fi
|
||||
|
||||
# Everything is passed via ENVIRON, not awk -v, to avoid awk interpreting
|
||||
# backslash escapes (colours hold literal \e sequences; a filter could
|
||||
# contain a backslash). The file is given twice: the first pass only
|
||||
# collects the active keys, the second one prints.
|
||||
_CONF_DUMP_SEC="$c_sec" \
|
||||
_CONF_DUMP_KEY_ACTIVE="$c_active" \
|
||||
_CONF_DUMP_KEY_DEFAULT="$c_default" \
|
||||
_CONF_DUMP_RST="$c_rst" \
|
||||
_CONF_DUMP_SECF="$section" \
|
||||
_CONF_DUMP_KEYF="$key_pattern" \
|
||||
awk -v show_all="$show_all" '
|
||||
BEGIN {
|
||||
c_sec = ENVIRON["_CONF_DUMP_SEC"]
|
||||
c_active = ENVIRON["_CONF_DUMP_KEY_ACTIVE"]
|
||||
c_default = ENVIRON["_CONF_DUMP_KEY_DEFAULT"]
|
||||
c_rst = ENVIRON["_CONF_DUMP_RST"]
|
||||
sec_filter = ENVIRON["_CONF_DUMP_SECF"]
|
||||
key_filter = ENVIRON["_CONF_DUMP_KEYF"]
|
||||
# Shell colour vars contain literal \e[…m strings; convert to
|
||||
# the actual ESC byte so awk print emits real ANSI sequences.
|
||||
gsub(/\\e/, "\033", c_sec)
|
||||
gsub(/\\e/, "\033", c_active)
|
||||
gsub(/\\e/, "\033", c_default)
|
||||
gsub(/\\e/, "\033", c_rst)
|
||||
in_target = 0
|
||||
current_sec = ""
|
||||
hdr_printed = 0
|
||||
found = 0
|
||||
# active[sec:key] — keys set (uncommented) anywhere in the file,
|
||||
# filled by the first pass.
|
||||
# seen[sec:key] — keys already printed, to show each commented
|
||||
# default at most once.
|
||||
}
|
||||
|
||||
# First pass (FNR == NR): record the active keys, print nothing.
|
||||
# An active key suppresses its commented default wherever the latter
|
||||
# appears in the section (before or after it).
|
||||
FNR == NR {
|
||||
line = $0; sub(/\r$/, "", line)
|
||||
if (line ~ /^[[:space:]]*\[[^]]+\][[:space:]]*$/) {
|
||||
pass1_sec = line
|
||||
sub(/^[[:space:]]*\[/, "", pass1_sec)
|
||||
sub(/\][[:space:]]*$/, "", pass1_sec)
|
||||
} else if (line ~ /^[[:space:]]*[A-Za-z_][A-Za-z0-9_]*[[:space:]]*=/) {
|
||||
k = line; sub(/[[:space:]]*=.*$/, "", k); sub(/^[[:space:]]*/, "", k)
|
||||
active[pass1_sec ":" k] = 1
|
||||
}
|
||||
next
|
||||
}
|
||||
|
||||
{
|
||||
sub(/\r$/, "")
|
||||
|
||||
# Section header (leading blanks allowed, as parse_conf does)
|
||||
if ($0 ~ /^[[:space:]]*\[[^]]+\][[:space:]]*$/) {
|
||||
current_sec = $0
|
||||
sub(/^[[:space:]]*\[/, "", current_sec)
|
||||
sub(/\][[:space:]]*$/, "", current_sec)
|
||||
in_target = (sec_filter == "" || current_sec == sec_filter)
|
||||
hdr_printed = 0
|
||||
next
|
||||
}
|
||||
|
||||
if (!in_target) next
|
||||
|
||||
# Active (uncommented) key=value — always shown
|
||||
if ($0 ~ /^[[:space:]]*[A-Za-z_][A-Za-z0-9_]*[[:space:]]*=/) {
|
||||
key = $0; sub(/[[:space:]]*=.*$/, "", key); sub(/^[[:space:]]*/, "", key)
|
||||
val = $0; sub(/^[^=]*=/, "", val)
|
||||
if (key_filter != "" && index(key, key_filter) == 0) next
|
||||
if (!hdr_printed) {
|
||||
if (found) print ""
|
||||
print c_sec "[" current_sec "]" c_rst
|
||||
hdr_printed = 1; found = 1
|
||||
}
|
||||
seen[current_sec ":" key] = 1
|
||||
print " " c_active key c_rst "=" val
|
||||
next
|
||||
}
|
||||
|
||||
# Commented-out key=value — shown only with show_all, and only when
|
||||
# the key is not set for real anywhere in the same section.
|
||||
# The value displayed is the live environment value (what load_conf
|
||||
# actually exported); when the key is not set in the environment we
|
||||
# fall back to the value written in the comment, i.e. the documented
|
||||
# default. Each key is shown at most once.
|
||||
if (show_all && $0 ~ /^[[:space:]]*#[[:space:]]*[A-Za-z_][A-Za-z0-9_]*[[:space:]]*=/) {
|
||||
line = $0; sub(/^[[:space:]]*#[[:space:]]*/, "", line)
|
||||
key = line; sub(/[[:space:]]*=.*$/, "", key); sub(/^[[:space:]]*/, "", key)
|
||||
if (active[current_sec ":" key] || seen[current_sec ":" key]) next
|
||||
seen[current_sec ":" key] = 1
|
||||
if (key_filter != "" && index(key, key_filter) == 0) next
|
||||
val = ENVIRON[key]
|
||||
if (val == "") { val = line; sub(/^[^=]*=[[:space:]]*/, "", val) }
|
||||
if (!hdr_printed) {
|
||||
if (found) print ""
|
||||
print c_sec "[" current_sec "]" c_rst
|
||||
hdr_printed = 1; found = 1
|
||||
}
|
||||
print " " c_default key c_rst "=" val
|
||||
}
|
||||
}
|
||||
' "$conf_file" "$conf_file"
|
||||
}
|
||||
export -f conf_dump
|
||||
# ------------------------------------------------------------------------------
|
||||
# Set TERM to the best terminal capability available on this system.
|
||||
# Called automatically at sourcing time.
|
||||
#
|
||||
# If TERM is already set to a specific value (not empty, not the sentinel
|
||||
# "smart"), it is honoured as-is — standard Unix behaviour.
|
||||
# If TERM is empty or set to "smart", multiplexer hints are checked first
|
||||
# (TMUX, STY), then terminal emulator hints (TERM_PROGRAM, VTE_VERSION,
|
||||
# WT_SESSION), then terminfo is probed in preference order:
|
||||
# xterm-256color → xterm-color → xterm → vt100. COLORTERM=truecolor|24bit
|
||||
# enables the *-direct (24-bit) entries in those probes.
|
||||
#
|
||||
# Usage: term_set
|
||||
term_set()
|
||||
{
|
||||
local _current="${TERM:-}"
|
||||
|
||||
# Specific value already set — nothing to do.
|
||||
if [[ -n "$_current" && "$_current" != "smart" ]]; then
|
||||
export TERM="$_current"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Return true when terminfo has an entry for the given terminal type.
|
||||
# (Nested functions are global in bash; it is removed with unset -f below.)
|
||||
local _term_has
|
||||
_term_has()
|
||||
{
|
||||
tput -T "$1" longname >/dev/null 2>&1
|
||||
}
|
||||
|
||||
# True-color flag: COLORTERM=truecolor|24bit is set by the emulator. It does
|
||||
# not select an entry by itself; it only enables the *-direct entries in the
|
||||
# probes below, each of which falls back to its *-256color counterpart.
|
||||
local _truecolor=0
|
||||
[[ "${COLORTERM:-}" == "truecolor" || "${COLORTERM:-}" == "24bit" ]] && _truecolor=1
|
||||
|
||||
local _candidate=""
|
||||
|
||||
# Probes run in priority order and stop at the first terminfo entry found
|
||||
# (_candidate non-empty). Multiplexers come first: they provide their own
|
||||
# terminal and inherit the emulator's variables (TERM_PROGRAM, VTE_VERSION,
|
||||
# ...), which would otherwise wrongly select the outer emulator's entry.
|
||||
|
||||
# 1. tmux — prefer *-direct when truecolor, then *-256color, then screen-256color.
|
||||
if [[ -n "${TMUX:-}" ]]; then
|
||||
if (( _truecolor )); then
|
||||
_term_has "tmux-direct" && _candidate="tmux-direct"
|
||||
[[ -z "$_candidate" ]] && _term_has "screen-direct" && _candidate="screen-direct"
|
||||
fi
|
||||
[[ -z "$_candidate" ]] && _term_has "tmux-256color" && _candidate="tmux-256color"
|
||||
[[ -z "$_candidate" ]] && _term_has "screen-256color" && _candidate="screen-256color"
|
||||
fi
|
||||
|
||||
# 2. GNU screen — prefer screen-direct when truecolor, then screen-256color.
|
||||
if [[ -z "$_candidate" && -n "${STY:-}" ]]; then
|
||||
if (( _truecolor )); then
|
||||
_term_has "screen-direct" && _candidate="screen-direct"
|
||||
fi
|
||||
[[ -z "$_candidate" ]] && _term_has "screen-256color" && _candidate="screen-256color"
|
||||
[[ -z "$_candidate" ]] && _term_has "screen" && _candidate="screen"
|
||||
fi
|
||||
|
||||
# 3. Terminal programme name hints.
|
||||
if [[ -z "$_candidate" ]]; then
|
||||
case "${TERM_PROGRAM:-}" in
|
||||
iTerm.app)
|
||||
if (( _truecolor )); then
|
||||
_term_has "xterm-direct" && _candidate="xterm-direct"
|
||||
fi
|
||||
[[ -z "$_candidate" ]] && _term_has "xterm-256color" && _candidate="xterm-256color"
|
||||
;;
|
||||
WezTerm)
|
||||
if (( _truecolor )); then
|
||||
_term_has "xterm-direct" && _candidate="xterm-direct"
|
||||
fi
|
||||
if [[ -z "$_candidate" ]]; then
|
||||
_term_has "wezterm" && _candidate="wezterm"
|
||||
fi
|
||||
[[ -z "$_candidate" ]] && _term_has "xterm-256color" && _candidate="xterm-256color"
|
||||
;;
|
||||
Hyper|vscode)
|
||||
if (( _truecolor )); then
|
||||
_term_has "xterm-direct" && _candidate="xterm-direct"
|
||||
fi
|
||||
[[ -z "$_candidate" ]] && _term_has "xterm-256color" && _candidate="xterm-256color"
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
|
||||
# 4. VTE-based terminals (GNOME Terminal, Tilix, Xfce Terminal, …).
|
||||
if [[ -z "$_candidate" && -n "${VTE_VERSION:-}" ]]; then
|
||||
if (( _truecolor )); then
|
||||
_term_has "vte-direct" && _candidate="vte-direct"
|
||||
fi
|
||||
[[ -z "$_candidate" ]] && _term_has "vte-256color" && _candidate="vte-256color"
|
||||
[[ -z "$_candidate" ]] && _term_has "xterm-256color" && _candidate="xterm-256color"
|
||||
fi
|
||||
|
||||
# 5. Windows Terminal.
|
||||
if [[ -z "$_candidate" && -n "${WT_SESSION:-}" ]]; then
|
||||
if (( _truecolor )); then
|
||||
_term_has "xterm-direct" && _candidate="xterm-direct"
|
||||
fi
|
||||
[[ -z "$_candidate" ]] && _term_has "xterm-256color" && _candidate="xterm-256color"
|
||||
fi
|
||||
|
||||
# 6. Generic terminfo probe in preference order.
|
||||
if [[ -z "$_candidate" ]]; then
|
||||
local _t
|
||||
for _t in xterm-256color xterm-color xterm vt100; do
|
||||
_term_has "$_t" && { _candidate="$_t"; break; }
|
||||
done
|
||||
fi
|
||||
|
||||
unset -f _term_has
|
||||
|
||||
export TERM="${_candidate:-vt100}"
|
||||
}
|
||||
export -f term_set
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
term_set
|
||||
|
||||
# EOF
|
||||
+204
-56
@@ -1,72 +1,220 @@
|
||||
#!/usr/bin/env bash
|
||||
# ------------------------------------------------------------------------------
|
||||
# Display a backtrace
|
||||
# Copyright (c) 2013-2026 Geoffray Levasseur <fatalerrors@geoffray-levasseur.org>
|
||||
# Protected by the BSD3 license. Please read bellow for details.
|
||||
#
|
||||
# * Redistribution and use in source and binary forms,
|
||||
# * with or without modification, are permitted provided
|
||||
# * that the following conditions are met:
|
||||
# *
|
||||
# * Redistributions of source code must retain the above
|
||||
# * copyright notice, this list of conditions and the
|
||||
# * following disclaimer.
|
||||
# *
|
||||
# * Redistributions in binary form must reproduce the above
|
||||
# * copyright notice, this list of conditions and the following
|
||||
# * disclaimer in the documentation and/or other materials
|
||||
# * provided with the distribution.
|
||||
# *
|
||||
# * Neither the name of the copyright holder nor the names
|
||||
# * of any other contributors may be used to endorse or
|
||||
# * promote products derived from this software without
|
||||
# * specific prior written permission.
|
||||
# *
|
||||
# * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
|
||||
# * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
|
||||
# * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
|
||||
# * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
# * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
|
||||
# * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
# * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
|
||||
# * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
# * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||
# * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
|
||||
# * OF SUCH DAMAGE.
|
||||
# ------------------------------------------------------------------------------
|
||||
function backtrace ()
|
||||
{
|
||||
echo "========= Call stack ========="
|
||||
typeset -i i=0
|
||||
|
||||
local func=
|
||||
for func in "${FUNCNAME[@]}"; do
|
||||
if [[ $i -ne 0 ]]; then
|
||||
printf '%15s() %s:%d\n' \
|
||||
"$func" "${BASH_SOURCE[$i]}" "${BASH_LINENO[ (( $i - 1)) ]}"
|
||||
fi
|
||||
let i++ || true
|
||||
done
|
||||
unset func i
|
||||
echo "=============================="
|
||||
}
|
||||
|
||||
# Common helpers (helpers/common.sh)
|
||||
# shellcheck source=../helpers/common.sh
|
||||
declare -F _profile_load_helpers >/dev/null || . "$(dirname -- "${BASH_SOURCE[0]}")/../helpers/common.sh"
|
||||
|
||||
load_conf "debug"
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# Function to be trapped for errors investigation
|
||||
# Display a backtrace of the current call stack (innermost frame first), one
|
||||
# line per frame: function, file where it is defined, line of the call site.
|
||||
# Usage: backtrace
|
||||
# shellcheck disable=SC2120 # only --help is a possible argument
|
||||
backtrace()
|
||||
{
|
||||
if [[ "${1:-}" == -h || "${1:-}" == --help ]]; then
|
||||
printf "backtrace: print the current call stack, innermost frame first.\n"
|
||||
printf "Usage: backtrace\n"
|
||||
return 0
|
||||
fi
|
||||
printf "========= Call stack =========\n"
|
||||
local i=1 # We begin at 1 to ignore backtrace itself
|
||||
while [[ $i -lt ${#FUNCNAME[@]} ]]; do
|
||||
# BASH_SOURCE[i] is the file defining FUNCNAME[i], and BASH_LINENO[i-1]
|
||||
# the line of that file from which FUNCNAME[i-1] was called.
|
||||
printf '%15s() %s:%d\n' \
|
||||
"${FUNCNAME[$i]}" "${BASH_SOURCE[$i]}" "${BASH_LINENO[$(( i-1 ))]}"
|
||||
i=$(( i + 1 ))
|
||||
done
|
||||
printf "==============================\n"
|
||||
}
|
||||
export -f backtrace
|
||||
# ------------------------------------------------------------------------------
|
||||
function error ()
|
||||
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# Handler installed by settrace as ERR trap: report the failure and the call
|
||||
# stack, then give back the exit status of the failed command so the trap does
|
||||
# not alter the shell behaviour (e.g. under "set -e").
|
||||
# Everything goes to stderr: with errtrace a failure inside $(...) or a pipe
|
||||
# would otherwise inject the trace into the captured output.
|
||||
# Exported because an exported settrace may be used from child bash processes,
|
||||
# where the trap needs this function to exist.
|
||||
error()
|
||||
{
|
||||
local errcode=$?
|
||||
backtrace
|
||||
printf "Command failed with exit status %d\n" "$errcode" >&2
|
||||
backtrace >&2
|
||||
return $errcode
|
||||
}
|
||||
|
||||
export -f error
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# Activate or deactivate error trapping to display backtrace
|
||||
# ------------------------------------------------------------------------------
|
||||
settrace ()
|
||||
# Usage: settrace <--on|--off|--status> [--force]
|
||||
# Startup behaviour is set by SETTRACE_DEFAULT_ENABLED in the [debug] section.
|
||||
settrace()
|
||||
{
|
||||
local status="off"
|
||||
[[ $(trap -p ERR) ]] && status="on"
|
||||
#trap -p ERR
|
||||
for opt in $@ ; do
|
||||
case $opt in
|
||||
"-h"|"--help")
|
||||
echo "Try to activate backtrace display for script debugging."
|
||||
echo
|
||||
echo "Options:"
|
||||
echo " --on Activate backtrace generation"
|
||||
echo " --off Deactivate backtrace generation"
|
||||
echo
|
||||
echo "That function active a trap event on error. If the script you want to"
|
||||
echo "debug overload the ERR bash trap, it will not work."
|
||||
echo
|
||||
;;
|
||||
"--on")
|
||||
if [[ $status == "on" ]]; then
|
||||
echo "Warning: ERR signal trap is already set, replacing previous trap!"
|
||||
fi
|
||||
trap "error" ERR
|
||||
;;
|
||||
"--off")
|
||||
if [[ $status != "on" ]]; then
|
||||
echo "Warning: ERR signal trap is already unset!"
|
||||
fi
|
||||
trap - ERR
|
||||
;;
|
||||
"--status")
|
||||
echo "ERR trap signal is ${status}."
|
||||
;;
|
||||
esac
|
||||
local PARSED
|
||||
PARSED=$(getopt -oh --long help,on,off,status,force -n settrace -- "$@")
|
||||
# shellcheck disable=SC2181 # getopt return code is checked immediately after
|
||||
if [[ $? -ne 0 ]]; then
|
||||
disp E "Invalid options, use \"settrace --help\" to display usage."
|
||||
return 1
|
||||
fi
|
||||
# getopt emits a shell-quoted, normalised argument list ("--" ends options)
|
||||
local -a _pargs=()
|
||||
_getopt_split _pargs "$PARSED"
|
||||
set -- "${_pargs[@]}"
|
||||
|
||||
# First pass: gather the requested action and flags. --on, --off and
|
||||
# --status are mutually exclusive; --force only modifies --on and --off.
|
||||
local action="" force=0
|
||||
while true; do
|
||||
case $1 in
|
||||
-h|--help)
|
||||
printf "Try to activate backtrace display for script debugging.\n\n"
|
||||
printf "Options:\n"
|
||||
printf "\t--on\t\tActivate backtrace generation\n"
|
||||
printf "\t--force\t\tReplace (with --on) or remove (with --off) an ERR trap\n"
|
||||
printf "\t\t\tnot installed by settrace\n"
|
||||
printf "\t--off\t\tDeactivate backtrace generation\n"
|
||||
printf "\t--status\tDisplay whether the ERR trap is set\n\n"
|
||||
printf "That function activates a trap event on error. If the script you want to\n"
|
||||
printf "debug overloads the ERR bash trap, it will not work.\n\n"
|
||||
printf "To activate it at every interactive startup, set SETTRACE_DEFAULT_ENABLED=1\n"
|
||||
printf "in the [debug] section of the profile configuration.\n"
|
||||
return 0
|
||||
;;
|
||||
--on|--off|--status)
|
||||
if [[ -n $action ]]; then
|
||||
disp E "The --on, --off and --status options are mutually exclusive."
|
||||
return 1
|
||||
fi
|
||||
action="${1#--}"
|
||||
shift
|
||||
;;
|
||||
--force)
|
||||
force=1
|
||||
shift
|
||||
;;
|
||||
--)
|
||||
shift
|
||||
break
|
||||
;;
|
||||
*)
|
||||
disp E "Invalid options, use \"settrace --help\" to display usage."
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
unset status
|
||||
|
||||
# Second pass: evaluate the current trap state, then apply the action.
|
||||
# "trap -p ERR" prints the exact command line that re-creates the trap, so
|
||||
# comparing it tells our own trap ("on") from one set by someone else
|
||||
# ("foreign"); empty output means no trap at all ("off").
|
||||
# Limitation: bash hides the ERR trap from code running in a function (and
|
||||
# its subshells) unless errtrace is set. Our own trap is always seen since
|
||||
# settrace sets errtrace with it, but a foreign trap set without errtrace is
|
||||
# reported as "off" (and can then be replaced without --force).
|
||||
local status="off" current
|
||||
current=$(trap -p ERR)
|
||||
if [[ -n $current ]]; then
|
||||
status="foreign"
|
||||
[[ $current == "trap -- 'error' ERR" ]] && status="on"
|
||||
fi
|
||||
|
||||
case $action in
|
||||
on)
|
||||
if [[ $status == "on" ]]; then
|
||||
disp W "ERR signal trap is already set."
|
||||
return 0
|
||||
fi
|
||||
if [[ $status == "foreign" && $force -eq 0 ]]; then
|
||||
disp E "Another ERR signal trap is already set. Use --force to replace it."
|
||||
return 1
|
||||
fi
|
||||
# errtrace makes functions and subshells inherit the ERR trap
|
||||
set -o errtrace
|
||||
trap 'error' ERR
|
||||
;;
|
||||
off)
|
||||
if [[ $status == "foreign" && $force -eq 0 ]]; then
|
||||
disp E "The ERR signal trap was not set by settrace. Use --force to remove it."
|
||||
return 1
|
||||
fi
|
||||
[[ $status == "off" ]] && disp W "ERR signal trap is already unset!"
|
||||
trap - ERR
|
||||
set +o errtrace
|
||||
;;
|
||||
status)
|
||||
case $status in
|
||||
foreign) disp I "Trap signal on error is set, but not by settrace." ;;
|
||||
*) disp I "Trap signal on error is ${status}." ;;
|
||||
esac
|
||||
;;
|
||||
"")
|
||||
disp E "No action specified, use \"settrace --help\" to display usage."
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
}
|
||||
export -f settrace
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# Optional trace activation at startup (SETTRACE_DEFAULT_ENABLED, default 0).
|
||||
# Interactive shells only: this file is sourced by non-interactive shells too,
|
||||
# where an ERR trap would add noise to scripts. [[ $- == *i* ]] is used because
|
||||
# profile.sh sets INTERACTIVE only after the modules are loaded.
|
||||
# disp is not defined yet (modules load alphabetically), so the trap is set only
|
||||
# when none exists, a path that never calls disp. An existing trap is left alone.
|
||||
# Written as "if" so the sourcing status stays 0 when nothing is done.
|
||||
case "${SETTRACE_DEFAULT_ENABLED:-0}" in
|
||||
1|[Yy][Ee][Ss]|[Tt][Rr][Uu][Ee]|[Oo][Nn])
|
||||
if [[ $- == *i* && -z $(trap -p ERR) ]]; then
|
||||
settrace --on
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
|
||||
# EOF
|
||||
|
||||
@@ -0,0 +1,849 @@
|
||||
#!/usr/bin/env bash
|
||||
# ------------------------------------------------------------------------------
|
||||
# Copyright (c) 2013-2026 Geoffray Levasseur <fatalerrors@geoffray-levasseur.org>
|
||||
# Protected by the BSD3 license. Please read bellow for details.
|
||||
#
|
||||
# * Redistribution and use in source and binary forms,
|
||||
# * with or without modification, are permitted provided
|
||||
# * that the following conditions are met:
|
||||
# *
|
||||
# * Redistributions of source code must retain the above
|
||||
# * copyright notice, this list of conditions and the
|
||||
# * following disclaimer.
|
||||
# *
|
||||
# * Redistributions in binary form must reproduce the above
|
||||
# * copyright notice, this list of conditions and the following
|
||||
# * disclaimer in the documentation and/or other materials
|
||||
# * provided with the distribution.
|
||||
# *
|
||||
# * Neither the name of the copyright holder nor the names
|
||||
# * of any other contributors may be used to endorse or
|
||||
# * promote products derived from this software without
|
||||
# * specific prior written permission.
|
||||
# *
|
||||
# * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
|
||||
# * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
|
||||
# * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
|
||||
# * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
# * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
|
||||
# * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
# * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
|
||||
# * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
# * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||
# * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
|
||||
# * OF SUCH DAMAGE.
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
# Common helpers (helpers/common.sh)
|
||||
# shellcheck source=../helpers/common.sh
|
||||
declare -F _profile_load_helpers >/dev/null || . "$(dirname -- "${BASH_SOURCE[0]}")/../helpers/common.sh"
|
||||
|
||||
load_conf disp
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# Color definitions
|
||||
set_colors()
|
||||
{
|
||||
# Standard 16 colors display declaration
|
||||
export DEFAULTFG='\e[0;39m'
|
||||
export DEFAULTBG='\e[0;49m'
|
||||
export DEFAULTCOL="${DEFAULTBG}${DEFAULTFG}"
|
||||
export RESETCOL='\e[0m'
|
||||
|
||||
# Regular Colors
|
||||
export Black='\e[0;30m'
|
||||
export Red='\e[0;31m'
|
||||
export Green='\e[0;32m'
|
||||
export Yellow='\e[0;33m'
|
||||
export Blue='\e[0;34m'
|
||||
export Purple='\e[0;35m'
|
||||
export Cyan='\e[0;36m'
|
||||
export White='\e[0;37m'
|
||||
|
||||
# Bold
|
||||
export BBlack='\e[1;30m'
|
||||
export BRed='\e[1;31m'
|
||||
export BGreen='\e[1;32m'
|
||||
export BYellow='\e[1;33m'
|
||||
export BBlue='\e[1;34m'
|
||||
export BPurple='\e[1;35m'
|
||||
export BCyan='\e[1;36m'
|
||||
export BWhite='\e[1;37m'
|
||||
|
||||
# Underline
|
||||
export UBlack='\e[4;30m'
|
||||
export URed='\e[4;31m'
|
||||
export UGreen='\e[4;32m'
|
||||
export UYellow='\e[4;33m'
|
||||
export UBlue='\e[4;34m'
|
||||
export UPurple='\e[4;35m'
|
||||
export UCyan='\e[4;36m'
|
||||
export UWhite='\e[4;37m'
|
||||
|
||||
# Background
|
||||
export On_Black='\e[40m'
|
||||
export On_Red='\e[41m'
|
||||
export On_Green='\e[42m'
|
||||
export On_Yellow='\e[43m'
|
||||
export On_Blue='\e[44m'
|
||||
export On_Purple='\e[45m'
|
||||
export On_Cyan='\e[46m'
|
||||
export On_White='\e[47m'
|
||||
|
||||
# High Intensity
|
||||
export IBlack='\e[0;90m'
|
||||
export IRed='\e[0;91m'
|
||||
export IGreen='\e[0;92m'
|
||||
export IYellow='\e[0;93m'
|
||||
export IBlue='\e[0;94m'
|
||||
export IPurple='\e[0;95m'
|
||||
export ICyan='\e[0;96m'
|
||||
export IWhite='\e[0;97m'
|
||||
|
||||
# Bold High Intensity
|
||||
export BIBlack='\e[1;90m'
|
||||
export BIRed='\e[1;91m'
|
||||
export BIGreen='\e[1;92m'
|
||||
export BIYellow='\e[1;93m'
|
||||
export BIBlue='\e[1;94m'
|
||||
export BIPurple='\e[1;95m'
|
||||
export BICyan='\e[1;96m'
|
||||
export BIWhite='\e[1;97m'
|
||||
|
||||
# High Intensity backgrounds
|
||||
export On_IBlack='\e[0;100m'
|
||||
export On_IRed='\e[0;101m'
|
||||
export On_IGreen='\e[0;102m'
|
||||
export On_IYellow='\e[0;103m'
|
||||
export On_IBlue='\e[0;104m'
|
||||
export On_IPurple='\e[0;105m'
|
||||
export On_ICyan='\e[0;106m'
|
||||
export On_IWhite='\e[0;107m'
|
||||
}
|
||||
export -f set_colors
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# Display a message
|
||||
# Usage: disp <type> <message>
|
||||
# Types:
|
||||
# I : info (green), stdout
|
||||
# W : warning (yellow), stderr
|
||||
# E : error (red), stderr
|
||||
# D : debug (cyan), stderr, shown only when DEBUG is an integer > 1
|
||||
# Without a recognised type (case-insensitive), the arguments are the message
|
||||
# itself, printed unprefixed on stdout like an info.
|
||||
#
|
||||
# Environment:
|
||||
# NO_COLOR non-empty: no colour or reset sequence is emitted
|
||||
# QUIET=1 hides info and unprefixed messages (warnings/errors remain)
|
||||
# COLUMNS terminal width used for wrapping (tput cols otherwise, 80 fallback)
|
||||
#
|
||||
# The message is wrapped to the terminal width on spaces, ANSI sequences count
|
||||
# for no width, and continuation lines are aligned after the prefix. Backslash
|
||||
# escapes (\t, \e, ...) are interpreted when printing (printf %b), but wrapping
|
||||
# is computed before that, so a literal "\t" counts as two characters.
|
||||
# A message hidden by QUIET/DEBUG still returns 0.
|
||||
disp()
|
||||
{
|
||||
local _disp_print_wrapped
|
||||
_disp_print_wrapped()
|
||||
{
|
||||
local prefix="$1"
|
||||
local target_fd="$2"
|
||||
shift 2
|
||||
local message="$*"
|
||||
|
||||
local cols="${COLUMNS:-}"
|
||||
if [[ -z "$cols" || ! "$cols" =~ ^[0-9]+$ || "$cols" -lt 20 ]]; then
|
||||
cols=$(tput cols 2>/dev/null)
|
||||
fi
|
||||
[[ -z "$cols" || ! "$cols" =~ ^[0-9]+$ || "$cols" -lt 20 ]] && cols=80
|
||||
|
||||
# Match ANSI CSI sequences in real-ESC, literal \e and literal \033 forms
|
||||
# so their on-screen width (zero) is excluded from all width calculations.
|
||||
local ansi_re=$'\e\\[[0-9;]*[A-Za-z]'
|
||||
ansi_re+='|\\e\[[0-9;]*[A-Za-z]'
|
||||
ansi_re+='|\\033\[[0-9;]*[A-Za-z]'
|
||||
|
||||
# The hanging-indent width is the prefix's VISIBLE length (ANSI stripped),
|
||||
# so callers no longer need to pass a separate plain-text length.
|
||||
local _pv=$prefix
|
||||
while [[ $_pv =~ $ansi_re ]]; do _pv=${_pv/"${BASH_REMATCH[0]}"/}; done
|
||||
local indent_len=0
|
||||
(( ${#_pv} > 0 )) && indent_len=$(( ${#_pv} + 1 ))
|
||||
|
||||
local width=$((cols - indent_len))
|
||||
(( width < 10 )) && width=10
|
||||
|
||||
# ANSI-aware soft wrap (fold -s semantics): break at the last space that
|
||||
# keeps a line within `width` VISIBLE columns, hard-breaking only when a
|
||||
# run has no space. ANSI escapes are copied through at zero width so they
|
||||
# no longer inflate the measured width and cause premature wrapping.
|
||||
local wrapped="" line="" line_vis=0 last_space=-1
|
||||
local _i=0 _n=${#message}
|
||||
while (( _i < _n )); do
|
||||
local _c=${message:_i:1}
|
||||
# Only a real ESC or a literal backslash can start an ANSI sequence;
|
||||
# testing this first avoids copying the message tail (and running
|
||||
# the regex) for every ordinary character.
|
||||
# shellcheck disable=SC1003 # a literal backslash
|
||||
if [[ $_c == $'\e' || $_c == '\' ]]; then
|
||||
local _sub=${message:_i}
|
||||
if [[ $_sub =~ ^($ansi_re) ]]; then
|
||||
line+=${BASH_REMATCH[0]}
|
||||
_i=$(( _i + ${#BASH_REMATCH[0]} ))
|
||||
continue
|
||||
fi
|
||||
fi
|
||||
_i=$(( _i + 1 ))
|
||||
line+=$_c
|
||||
(( ++line_vis ))
|
||||
[[ $_c == ' ' ]] && last_space=${#line}
|
||||
if (( line_vis > width )); then
|
||||
if (( last_space > 0 )); then
|
||||
wrapped+=${line:0:last_space-1}$'\n'
|
||||
line=${line:last_space}
|
||||
local _st=$line
|
||||
while [[ $_st =~ $ansi_re ]]; do _st=${_st/"${BASH_REMATCH[0]}"/}; done
|
||||
line_vis=${#_st}
|
||||
else
|
||||
local _keep=${line: -1}
|
||||
wrapped+=${line:0:${#line}-1}$'\n'
|
||||
line=$_keep
|
||||
line_vis=1
|
||||
fi
|
||||
last_space=-1
|
||||
fi
|
||||
done
|
||||
wrapped+=$line
|
||||
|
||||
# RESETCOL closes any colour left open by the message. It is skipped
|
||||
# under NO_COLOR so that no escape sequence at all is emitted.
|
||||
local reset="${RESETCOL:-}"
|
||||
[[ -n ${NO_COLOR:-} ]] && reset=""
|
||||
|
||||
# >&"$target_fd" sends the output to stdout (1) or stderr (2). The
|
||||
# first line carries the prefix, continuation lines are indented to
|
||||
# line up with the message body.
|
||||
local first_line=1
|
||||
local line
|
||||
while IFS= read -r line || [[ -n "$line" ]]; do
|
||||
if (( first_line )); then
|
||||
if [[ -n "$prefix" ]]; then
|
||||
printf "%b\n" "${prefix} ${line}${reset}" >&"$target_fd"
|
||||
else
|
||||
printf "%b\n" "${line}${reset}" >&"$target_fd"
|
||||
fi
|
||||
first_line=0
|
||||
else
|
||||
printf "%*s%b\n" "$indent_len" "" "${line}${reset}" >&"$target_fd"
|
||||
fi
|
||||
done <<< "$wrapped"
|
||||
}
|
||||
|
||||
# Handle NO_COLOR: disable colors if set
|
||||
local color_enabled=1
|
||||
[[ -n ${NO_COLOR:-} ]] && color_enabled=0
|
||||
|
||||
# Each branch ends with an "if" (never a bare "[[ ]] && cmd") so that a
|
||||
# message hidden by QUIET/DEBUG still returns 0: a non-zero status would
|
||||
# trip "set -e", ERR traps, and "disp ... && next" chains.
|
||||
case ${1^^} in
|
||||
"I")
|
||||
if [[ $color_enabled -eq 1 ]]; then
|
||||
local heads="[ ${IGreen}info${DEFAULTFG} ]"
|
||||
else
|
||||
local heads="[ info ]"
|
||||
fi
|
||||
shift
|
||||
if [[ ${QUIET:-} != 1 ]]; then
|
||||
_disp_print_wrapped "$heads" 1 "$*"
|
||||
fi
|
||||
;;
|
||||
"W")
|
||||
if [[ $color_enabled -eq 1 ]]; then
|
||||
local heads="[ ${IYellow}Warning${DEFAULTFG} ]"
|
||||
else
|
||||
local heads="[ Warning ]"
|
||||
fi
|
||||
shift
|
||||
_disp_print_wrapped "$heads" 2 "$*"
|
||||
;;
|
||||
"E")
|
||||
if [[ $color_enabled -eq 1 ]]; then
|
||||
local heads="[ ${IRed}ERROR${DEFAULTFG} ]"
|
||||
else
|
||||
local heads="[ ERROR ]"
|
||||
fi
|
||||
shift
|
||||
_disp_print_wrapped "$heads" 2 "$*"
|
||||
;;
|
||||
"D")
|
||||
if [[ $color_enabled -eq 1 ]]; then
|
||||
local heads="[ ${ICyan}debug${DEFAULTFG} ]"
|
||||
else
|
||||
local heads="[ debug ]"
|
||||
fi
|
||||
shift
|
||||
# Shown only when DEBUG is an integer greater than 1; sent to stderr
|
||||
# like the other diagnostics, so it never pollutes captured output.
|
||||
if [[ ${DEBUG:-} =~ ^[0-9]+$ && $DEBUG -gt 1 ]]; then
|
||||
_disp_print_wrapped "$heads" 2 "$*"
|
||||
fi
|
||||
;;
|
||||
* )
|
||||
# No (known) type: the whole argument list is the message, as for info
|
||||
if [[ ${QUIET:-} != 1 ]]; then
|
||||
_disp_print_wrapped "" 1 "$*"
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
}
|
||||
export -f disp
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# Render Markdown files with terminal formatting
|
||||
# Usage: mdcat [file]
|
||||
mdcat()
|
||||
{
|
||||
local _mdcat_style_inline
|
||||
_mdcat_style_inline()
|
||||
{
|
||||
local text="$1"
|
||||
local bold_on="" italic_on="" code_on="" style_off=""
|
||||
local link_text_on="" link_url_on=""
|
||||
|
||||
if [[ -z $NO_COLOR ]]; then
|
||||
bold_on=$'\e[1m'
|
||||
italic_on=$'\e[3m'
|
||||
code_on="${On_IBlack}${BIWhite}"
|
||||
link_text_on=$'\e[4;96m'
|
||||
link_url_on="$IBlack"
|
||||
style_off="$RESETCOL"
|
||||
fi
|
||||
|
||||
# Inline transforms. Code spans and backslash-escaped punctuation
|
||||
# (\<, \>, \_, \*, ...) are lifted out first and replaced by numbered
|
||||
# placeholders (\x01N\x02 and \x03N\x04), so that nothing inside them is
|
||||
# reinterpreted (links, emphasis); they are put back, code spans styled,
|
||||
# at the very end. Then links, bare URLs and emphasis.
|
||||
local -a spans=() escs=()
|
||||
local ph_open=$'\x01' ph_close=$'\x02' esc_open=$'\x03' esc_close=$'\x04'
|
||||
while [[ "$text" =~ \`([^\`]+)\` ]]; do
|
||||
local match="${BASH_REMATCH[0]}"
|
||||
local before="${text%%"$match"*}"
|
||||
local after="${text#*"$match"}"
|
||||
spans+=("${BASH_REMATCH[1]}")
|
||||
text="${before}${ph_open}$(( ${#spans[@]} - 1 ))${ph_close}${after}"
|
||||
done
|
||||
while [[ "$text" =~ \\([[:punct:]]) ]]; do
|
||||
local match="${BASH_REMATCH[0]}"
|
||||
local before="${text%%"$match"*}"
|
||||
local after="${text#*"$match"}"
|
||||
escs+=("${BASH_REMATCH[1]}")
|
||||
text="${before}${esc_open}$(( ${#escs[@]} - 1 ))${esc_close}${after}"
|
||||
done
|
||||
|
||||
# Links: [label](url)
|
||||
while [[ "$text" =~ \[([^][]+)\]\(([^()]+)\) ]]; do
|
||||
local match="${BASH_REMATCH[0]}"
|
||||
local label="${BASH_REMATCH[1]}"
|
||||
local url="${BASH_REMATCH[2]}"
|
||||
local before="${text%%"$match"*}"
|
||||
local after="${text#*"$match"}"
|
||||
if [[ "$label" == "$url" ]]; then
|
||||
if [[ -z $NO_COLOR ]]; then
|
||||
text="${before}${link_text_on}${label}${style_off}${after}"
|
||||
else
|
||||
text="${before}${label}${after}"
|
||||
fi
|
||||
elif [[ -z $NO_COLOR ]]; then
|
||||
text="${before}${link_text_on}${label}${style_off} ${link_url_on}(${url})${style_off}${after}"
|
||||
else
|
||||
text="${before}${label} (${url})${after}"
|
||||
fi
|
||||
done
|
||||
|
||||
# Style bare URLs without re-matching the same URL forever.
|
||||
# The prefix capture keeps progress monotonic and prevents freeze loops.
|
||||
# Skip this transformation in NO_COLOR mode.
|
||||
if [[ -z $NO_COLOR ]]; then
|
||||
while [[ "$text" =~ (^|[[:space:]\(])(https?://[^[:space:]\)]+) ]]; do
|
||||
local match="${BASH_REMATCH[0]}"
|
||||
local pre="${BASH_REMATCH[1]}"
|
||||
local url="${BASH_REMATCH[2]}"
|
||||
local before="${text%%"$match"*}"
|
||||
local after="${text#*"$match"}"
|
||||
text="${before}${pre}${link_text_on}${url}${style_off}${after}"
|
||||
done
|
||||
fi
|
||||
|
||||
while [[ "$text" =~ \*\*([^*]+)\*\* ]]; do
|
||||
local match="${BASH_REMATCH[0]}"
|
||||
local val="${BASH_REMATCH[1]}"
|
||||
local before="${text%%"$match"*}"
|
||||
local after="${text#*"$match"}"
|
||||
text="${before}${bold_on}${val}${style_off}${after}"
|
||||
done
|
||||
|
||||
while [[ "$text" =~ __([^_]+)__ ]]; do
|
||||
local match="${BASH_REMATCH[0]}"
|
||||
local val="${BASH_REMATCH[1]}"
|
||||
local before="${text%%"$match"*}"
|
||||
local after="${text#*"$match"}"
|
||||
text="${before}${bold_on}${val}${style_off}${after}"
|
||||
done
|
||||
|
||||
while [[ "$text" =~ \*([^*]+)\* ]]; do
|
||||
local match="${BASH_REMATCH[0]}"
|
||||
local val="${BASH_REMATCH[1]}"
|
||||
local before="${text%%"$match"*}"
|
||||
local after="${text#*"$match"}"
|
||||
text="${before}${italic_on}${val}${style_off}${after}"
|
||||
done
|
||||
|
||||
# Match _italic_ only when underscores are outside word-like identifiers.
|
||||
while [[ "$text" =~ (^|[[:space:][:punct:]])_([^[:space:]_][^_]*[^[:space:]_])_([[:space:][:punct:]]|$) ]]; do
|
||||
local match="${BASH_REMATCH[0]}"
|
||||
local pre="${BASH_REMATCH[1]}"
|
||||
local val="${BASH_REMATCH[2]}"
|
||||
local post="${BASH_REMATCH[3]}"
|
||||
local before="${text%%"$match"*}"
|
||||
local after="${text#*"$match"}"
|
||||
text="${before}${pre}${italic_on}${val}${style_off}${post}${after}"
|
||||
done
|
||||
|
||||
# Put the styled code spans and the escaped characters back (see the
|
||||
# placeholders above).
|
||||
local idx
|
||||
for idx in "${!spans[@]}"; do
|
||||
text="${text/"${ph_open}${idx}${ph_close}"/"${code_on}${spans[idx]}${style_off}"}"
|
||||
done
|
||||
for idx in "${!escs[@]}"; do
|
||||
text="${text/"${esc_open}${idx}${esc_close}"/"${escs[idx]}"}"
|
||||
done
|
||||
|
||||
printf "%s\n" "$text"
|
||||
}
|
||||
|
||||
local _mdcat_print_hr
|
||||
_mdcat_print_hr()
|
||||
{
|
||||
local cols="${_mdcat_cols:-}"
|
||||
if [[ -z "$cols" || ! "$cols" =~ ^[0-9]+$ || "$cols" -lt 20 ]]; then
|
||||
cols=$(tput cols 2>/dev/null)
|
||||
fi
|
||||
[[ -z "$cols" || ! "$cols" =~ ^[0-9]+$ || "$cols" -lt 20 ]] && cols=80
|
||||
|
||||
local hr
|
||||
printf -v hr "%*s" "$cols" ""
|
||||
hr="${hr// /-}"
|
||||
if [[ -z $NO_COLOR ]]; then
|
||||
printf "%b%s%b\n" "$IBlack" "$hr" "$RESETCOL"
|
||||
else
|
||||
printf "%s\n" "$hr"
|
||||
fi
|
||||
}
|
||||
|
||||
local _mdcat_print_code_block
|
||||
_mdcat_print_code_block()
|
||||
{
|
||||
local lang="$1"
|
||||
shift
|
||||
local -a lines=("$@")
|
||||
|
||||
local cols="${_mdcat_cols:-}"
|
||||
if [[ -z "$cols" || ! "$cols" =~ ^[0-9]+$ || "$cols" -lt 20 ]]; then
|
||||
cols=$(tput cols 2>/dev/null)
|
||||
fi
|
||||
[[ -z "$cols" || ! "$cols" =~ ^[0-9]+$ || "$cols" -lt 20 ]] && cols=80
|
||||
|
||||
local max_inner=$((cols - 4))
|
||||
(( max_inner < 16 )) && max_inner=16
|
||||
|
||||
local width=16 line
|
||||
for line in "${lines[@]}"; do
|
||||
local line_len=${#line}
|
||||
(( line_len > width )) && width=$line_len
|
||||
done
|
||||
(( width > max_inner )) && width=$max_inner
|
||||
|
||||
local border
|
||||
printf -v border "+-%*s-+" "$width" ""
|
||||
border="${border// /-}"
|
||||
|
||||
local frame_on="" code_on="" off=""
|
||||
if [[ -z $NO_COLOR ]]; then
|
||||
frame_on="$IBlack"
|
||||
code_on="$BIWhite"
|
||||
off="$RESETCOL"
|
||||
fi
|
||||
|
||||
printf "%b%s%b\n" "$frame_on" "$border" "$off"
|
||||
if [[ -n "$lang" ]]; then
|
||||
local tag="language: $lang"
|
||||
local wrapped_tag
|
||||
wrapped_tag=$(printf "%s" "$tag" | fold -s -w "$width")
|
||||
while IFS= read -r line || [[ -n "$line" ]]; do
|
||||
printf "%b| %b%-*s%b |%b\n" "$frame_on" "$code_on" "$width" "$line" "$frame_on" "$off"
|
||||
done <<< "$wrapped_tag"
|
||||
printf "%b%s%b\n" "$frame_on" "$border" "$off"
|
||||
fi
|
||||
|
||||
if [[ ${#lines[@]} -eq 0 ]]; then
|
||||
printf "%b| %b%-*s%b |%b\n" "$frame_on" "$code_on" "$width" "" "$frame_on" "$off"
|
||||
else
|
||||
for line in "${lines[@]}"; do
|
||||
local wrapped
|
||||
wrapped=$(printf "%s" "$line" | fold -s -w "$width")
|
||||
while IFS= read -r wline || [[ -n "$wline" ]]; do
|
||||
printf "%b| %b%-*s%b |%b\n" "$frame_on" "$code_on" "$width" "$wline" "$frame_on" "$off"
|
||||
done <<< "$wrapped"
|
||||
done
|
||||
fi
|
||||
printf "%b%s%b\n" "$frame_on" "$border" "$off"
|
||||
}
|
||||
|
||||
local _mdcat_print_table
|
||||
# That function is a bit slow, we need to try to optimize it
|
||||
_mdcat_print_table()
|
||||
{
|
||||
local _mdcat_parse_table_row
|
||||
# Split one table row into cells, joined with $sep (set by the caller,
|
||||
# reached through dynamic scoping). Cells are separated by '|' only; an
|
||||
# escaped "\|" is not supported. Trimming uses parameter expansion
|
||||
# (strip leading, then trailing, whitespace) to avoid forking sed.
|
||||
_mdcat_parse_table_row()
|
||||
{
|
||||
local input="$1" j
|
||||
local line="$input"
|
||||
line="${line#"${line%%[![:space:]]*}"}"
|
||||
line="${line%"${line##*[![:space:]]}"}"
|
||||
line="${line#|}"
|
||||
line="${line%|}"
|
||||
|
||||
# Parse Markdown cells using only '|' separators and preserve spaces inside cells.
|
||||
local -a cells=()
|
||||
local cell rest="$line"
|
||||
while :; do
|
||||
if [[ "$rest" == *'|'* ]]; then
|
||||
cell="${rest%%|*}"
|
||||
rest="${rest#*|}"
|
||||
else
|
||||
cell="$rest"
|
||||
rest=""
|
||||
fi
|
||||
cell="${cell#"${cell%%[![:space:]]*}"}"
|
||||
cell="${cell%"${cell##*[![:space:]]}"}"
|
||||
cells+=("$cell")
|
||||
[[ -z "$rest" ]] && break
|
||||
done
|
||||
|
||||
if [[ ${#cells[@]} -eq 0 ]]; then
|
||||
printf '%s' ""
|
||||
return
|
||||
fi
|
||||
|
||||
local joined="${cells[0]}"
|
||||
for ((j=1; j<${#cells[@]}; ++j)); do
|
||||
joined+="$sep${cells[j]}"
|
||||
done
|
||||
printf '%s' "$joined"
|
||||
}
|
||||
|
||||
local -a lines=("$@")
|
||||
local -a table_rows=()
|
||||
local -a col_widths=()
|
||||
local -A cell_styled=() cell_vislen=()
|
||||
local i j ncols=0 orow=0
|
||||
local sep=$'\x1f'
|
||||
|
||||
# Parse header and data rows, skipping the Markdown separator row.
|
||||
# Each cell is styled exactly once here; the styled text and its visible
|
||||
# width (ANSI stripped) are cached and reused when drawing, so the costly
|
||||
# _mdcat_style_inline runs once per cell instead of twice.
|
||||
for ((i=0; i<${#lines[@]}; ++i)); do
|
||||
(( i == 1 )) && continue
|
||||
local parsed
|
||||
parsed=$(_mdcat_parse_table_row "${lines[i]}")
|
||||
table_rows+=("$parsed")
|
||||
|
||||
local -a row=()
|
||||
IFS="$sep" read -r -a row <<< "$parsed"
|
||||
(( ncols < ${#row[@]} )) && ncols=${#row[@]}
|
||||
for ((j=0; j<${#row[@]}; ++j)); do
|
||||
local styled visible key="$orow:$j"
|
||||
styled=$(_mdcat_style_inline "${row[j]}")
|
||||
# Visible width: expand the literal \e sequences, then strip
|
||||
# every ANSI CSI sequence (no sed fork per cell).
|
||||
printf -v visible '%b' "$styled"
|
||||
while [[ $visible =~ $'\e'\[[0-9\;]*[A-Za-z] ]]; do
|
||||
visible=${visible/"${BASH_REMATCH[0]}"/}
|
||||
done
|
||||
cell_styled["$key"]="$styled"
|
||||
cell_vislen["$key"]=${#visible}
|
||||
[[ -z "${col_widths[j]}" ]] && col_widths[j]=0
|
||||
(( col_widths[j] < ${#visible} )) && col_widths[j]=${#visible}
|
||||
done
|
||||
orow=$((orow + 1))
|
||||
done
|
||||
|
||||
# Ensure all width slots are initialized before drawing borders.
|
||||
for ((j=0; j<ncols; ++j)); do
|
||||
[[ -z "${col_widths[j]}" ]] && col_widths[j]=0
|
||||
done
|
||||
|
||||
# Colours are dropped under NO_COLOR, like everywhere else in mdcat.
|
||||
local c_frame="$IBlack" c_head="$BBlue" c_off="$RESETCOL" c_cell="$RESETCOL"
|
||||
if [[ -n ${NO_COLOR:-} ]]; then
|
||||
c_frame="" c_head="" c_off="" c_cell=""
|
||||
fi
|
||||
|
||||
# Draw top border: one run of dashes per column (cell width + 2 spaces)
|
||||
local border="+" seg
|
||||
for ((j=0; j<ncols; ++j)); do
|
||||
printf -v seg '%*s' "$((col_widths[j] + 2))" ""
|
||||
border+="${seg// /-}+"
|
||||
done
|
||||
printf "%b\n" "$c_frame$border$c_off"
|
||||
|
||||
# Print header row (cached styled cells)
|
||||
printf "%b|" "$c_frame"
|
||||
for ((j=0; j<ncols; ++j)); do
|
||||
local hkey="0:$j"
|
||||
local styled_cell="${cell_styled[$hkey]:-}"
|
||||
local pad=$((col_widths[j] - ${cell_vislen[$hkey]:-0}))
|
||||
(( pad < 0 )) && pad=0
|
||||
printf " %b%b%*s%b |" "$c_head" "$styled_cell" "$pad" "" "$c_frame"
|
||||
done
|
||||
printf "%b\n" "$c_off"
|
||||
|
||||
# Header separator
|
||||
printf "%b|" "$c_frame"
|
||||
for ((j=0; j<ncols; ++j)); do
|
||||
printf -v seg '%*s' "${col_widths[j]}" ""
|
||||
printf " %s |" "${seg// /-}"
|
||||
done
|
||||
printf "%b\n" "$c_off"
|
||||
|
||||
# Print data rows (cached styled cells)
|
||||
for ((i=1; i<${#table_rows[@]}; ++i)); do
|
||||
printf "%b|" "$c_frame"
|
||||
for ((j=0; j<ncols; ++j)); do
|
||||
local key="$i:$j"
|
||||
local styled_cell="${cell_styled[$key]:-}"
|
||||
local pad=$((col_widths[j] - ${cell_vislen[$key]:-0}))
|
||||
(( pad < 0 )) && pad=0
|
||||
printf " %b%b%*s%b |" "$c_cell" "$styled_cell" "$pad" "" "$c_frame"
|
||||
done
|
||||
printf "%b\n" "$c_off"
|
||||
done
|
||||
|
||||
# Draw bottom border
|
||||
printf "%b\n" "$c_frame$border$c_off"
|
||||
}
|
||||
|
||||
local PARSED
|
||||
PARSED=$(getopt -o h --long help -n 'mdcat' -- "$@")
|
||||
# shellcheck disable=SC2181 # getopt return code is checked immediately after
|
||||
if [[ $? -ne 0 ]]; then
|
||||
disp E "Invalid options, use \"mdcat --help\" to display usage."
|
||||
return 1
|
||||
fi
|
||||
|
||||
# getopt emits a shell-quoted, normalised argument list ("--" ends options)
|
||||
local -a _pargs=()
|
||||
_getopt_split _pargs "$PARSED"
|
||||
set -- "${_pargs[@]}"
|
||||
while true; do
|
||||
case "$1" in
|
||||
-h|--help)
|
||||
printf "mdcat: Render a Markdown file with terminal formatting.\n"
|
||||
printf "Usage: mdcat [file]\n"
|
||||
printf "If no file is provided, mdcat reads from standard input.\n"
|
||||
return 0
|
||||
;;
|
||||
--)
|
||||
shift
|
||||
break
|
||||
;;
|
||||
*)
|
||||
disp E "Invalid option, use \"mdcat --help\" to display options list"
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ $# -gt 1 ]]; then
|
||||
disp E "Too many arguments. Usage: mdcat [file]"
|
||||
return 1
|
||||
fi
|
||||
|
||||
local input_file=""
|
||||
if [[ $# -eq 1 ]]; then
|
||||
input_file="$1"
|
||||
if [[ ! -f "$input_file" ]]; then
|
||||
disp E "File not found: $input_file"
|
||||
return 1
|
||||
fi
|
||||
if [[ ! -r "$input_file" ]]; then
|
||||
disp E "File is not readable: $input_file"
|
||||
return 1
|
||||
fi
|
||||
elif [[ -t 0 ]]; then
|
||||
disp E "No input provided. Usage: mdcat [file] or: cat file.md | mdcat"
|
||||
return 1
|
||||
fi
|
||||
|
||||
local in_code=0 code_lang="" raw line
|
||||
local -a code_lines=()
|
||||
local in_table=0
|
||||
local -a table_lines=()
|
||||
|
||||
# Resolve the terminal width once for the whole render instead of shelling
|
||||
# out to tput for every printed line.
|
||||
local _mdcat_cols="${COLUMNS:-}"
|
||||
if [[ -z "$_mdcat_cols" || ! "$_mdcat_cols" =~ ^[0-9]+$ || "$_mdcat_cols" -lt 20 ]]; then
|
||||
_mdcat_cols=$(tput cols 2>/dev/null)
|
||||
fi
|
||||
[[ -z "$_mdcat_cols" || ! "$_mdcat_cols" =~ ^[0-9]+$ || "$_mdcat_cols" -lt 20 ]] && _mdcat_cols=80
|
||||
|
||||
# Single-line push-back buffer: table detection reads one line ahead, so a
|
||||
# non-separator look-ahead line must be reprocessed rather than discarded.
|
||||
local pending_line="" has_pending=0
|
||||
while :; do
|
||||
if [[ $has_pending -eq 1 ]]; then
|
||||
raw="$pending_line"
|
||||
has_pending=0
|
||||
elif ! IFS= read -r raw; then
|
||||
[[ -n "$raw" ]] || break
|
||||
fi
|
||||
line="${raw%$'\r'}"
|
||||
|
||||
# Table detection: line with |, next line with | and ---
|
||||
# (never inside a fenced code block, where a table is just sample text)
|
||||
if [[ $in_code -eq 0 && $in_table -eq 0 && "$line" =~ ^[[:space:]]*\|.*\|[[:space:]]*$ ]]; then
|
||||
local next="" next_ok=1
|
||||
IFS= read -r next || next_ok=0
|
||||
# Accept: | --- | --- | or |:---|---:| etc.
|
||||
if [[ "$next" =~ ^[[:space:]]*\|[[:space:]]*:?[-]+:?([[:space:]]*\|[[:space:]]*:?[ -]+:?)*\|[[:space:]]*$ ]]; then
|
||||
in_table=1
|
||||
table_lines=("$line" "$next")
|
||||
continue
|
||||
fi
|
||||
# Not a separator: push the look-ahead line back so it is not lost.
|
||||
if [[ $next_ok -eq 1 || -n "$next" ]]; then
|
||||
pending_line="$next"
|
||||
has_pending=1
|
||||
fi
|
||||
fi
|
||||
if [[ $in_table -eq 1 ]]; then
|
||||
# Accept table row if it starts and ends with |
|
||||
if [[ "$line" =~ ^[[:space:]]*\|.*\|[[:space:]]*$ && ! "$line" =~ ^[[:space:]]*\|[[:space:]]*:?[-]+:?([[:space:]]*\|[[:space:]]*:?[ -]+:?)*\|[[:space:]]*$ ]]; then
|
||||
table_lines+=("$line")
|
||||
continue
|
||||
else
|
||||
_mdcat_print_table "${table_lines[@]}"
|
||||
in_table=0
|
||||
table_lines=()
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ $in_code -eq 1 ]]; then
|
||||
if [[ "$line" =~ ^\`\`\` ]]; then
|
||||
_mdcat_print_code_block "$code_lang" "${code_lines[@]}"
|
||||
in_code=0
|
||||
code_lang=""
|
||||
code_lines=()
|
||||
else
|
||||
code_lines+=("$line")
|
||||
fi
|
||||
continue
|
||||
fi
|
||||
|
||||
if [[ "$line" =~ ^\`\`\`[[:space:]]*([^[:space:]]*) ]]; then
|
||||
in_code=1
|
||||
code_lang="${BASH_REMATCH[1]}"
|
||||
code_lines=()
|
||||
continue
|
||||
fi
|
||||
|
||||
if [[ "$line" =~ ^(#{1,6})[[:space:]]+(.*)$ ]]; then
|
||||
local lvl=${#BASH_REMATCH[1]}
|
||||
local title="${BASH_REMATCH[2]}"
|
||||
local h_on=""
|
||||
if [[ -z $NO_COLOR ]]; then
|
||||
case "$lvl" in
|
||||
1) h_on="$BBlue" ;;
|
||||
2) h_on="$BCyan" ;;
|
||||
3) h_on="$BGreen" ;;
|
||||
*) h_on="$BIWhite" ;;
|
||||
esac
|
||||
fi
|
||||
local h_off="$RESETCOL"
|
||||
[[ -n $NO_COLOR ]] && h_off=""
|
||||
printf "%b%s%b\n" "$h_on" "$title" "$h_off"
|
||||
continue
|
||||
fi
|
||||
|
||||
if [[ "$line" =~ ^[[:space:]]*([\-*_])[[:space:]]*\1[[:space:]]*\1[\-*_[:space:]]*$ ]]; then
|
||||
_mdcat_print_hr
|
||||
continue
|
||||
fi
|
||||
|
||||
if [[ "$line" =~ ^([[:space:]]*)\>[[:space:]]?(.*)$ ]]; then
|
||||
local quote="${BASH_REMATCH[2]}"
|
||||
quote=$(_mdcat_style_inline "$quote")
|
||||
if [[ -z $NO_COLOR ]]; then
|
||||
printf "%s%b|%b %b\n" "${BASH_REMATCH[1]}" "$ICyan" "$RESETCOL" "$quote"
|
||||
else
|
||||
printf "%s| %b\n" "${BASH_REMATCH[1]}" "$quote"
|
||||
fi
|
||||
continue
|
||||
fi
|
||||
|
||||
if [[ "$line" =~ ^([[:space:]]*)[-+*][[:space:]]+(.*)$ ]]; then
|
||||
local item="${BASH_REMATCH[2]}"
|
||||
item=$(_mdcat_style_inline "$item")
|
||||
if [[ -z $NO_COLOR ]]; then
|
||||
printf "%s%b*%b %b\n" "${BASH_REMATCH[1]}" "$IGreen" "$RESETCOL" "$item"
|
||||
else
|
||||
printf "%s* %b\n" "${BASH_REMATCH[1]}" "$item"
|
||||
fi
|
||||
continue
|
||||
fi
|
||||
|
||||
if [[ "$line" =~ ^([[:space:]]*)([0-9]+)\.[[:space:]]+(.*)$ ]]; then
|
||||
local nitem="${BASH_REMATCH[3]}"
|
||||
nitem=$(_mdcat_style_inline "$nitem")
|
||||
if [[ -z $NO_COLOR ]]; then
|
||||
printf "%s%b%s.%b %b\n" "${BASH_REMATCH[1]}" "$IGreen" "${BASH_REMATCH[2]}" "$RESETCOL" "$nitem"
|
||||
else
|
||||
printf "%s%s. %b\n" "${BASH_REMATCH[1]}" "${BASH_REMATCH[2]}" "$nitem"
|
||||
fi
|
||||
continue
|
||||
fi
|
||||
|
||||
printf "%b\n" "$(_mdcat_style_inline "$line")"
|
||||
done < "${input_file:-/dev/stdin}"
|
||||
|
||||
# Flush any block still open at end-of-input.
|
||||
if [[ $in_code -eq 1 ]]; then
|
||||
_mdcat_print_code_block "$code_lang" "${code_lines[@]}"
|
||||
fi
|
||||
if [[ $in_table -eq 1 ]]; then
|
||||
_mdcat_print_table "${table_lines[@]}"
|
||||
fi
|
||||
}
|
||||
export -f mdcat
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
|
||||
# Load color codes
|
||||
set_colors
|
||||
|
||||
# EOF
|
||||
+986
-126
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,532 @@
|
||||
#!/usr/bin/env bash
|
||||
# ------------------------------------------------------------------------------
|
||||
# Copyright (c) 2013-2026 Geoffray Levasseur <fatalerrors@geoffray-levasseur.org>
|
||||
# Protected by the BSD3 license. Please read bellow for details.
|
||||
#
|
||||
# * Redistribution and use in source and binary forms,
|
||||
# * with or without modification, are permitted provided
|
||||
# * that the following conditions are met:
|
||||
# *
|
||||
# * Redistributions of source code must retain the above
|
||||
# * copyright notice, this list of conditions and the
|
||||
# * following disclaimer.
|
||||
# *
|
||||
# * Redistributions in binary form must reproduce the above
|
||||
# * copyright notice, this list of conditions and the following
|
||||
# * disclaimer in the documentation and/or other materials
|
||||
# * provided with the distribution.
|
||||
# *
|
||||
# * Neither the name of the copyright holder nor the names
|
||||
# * of any other contributors may be used to endorse or
|
||||
# * promote products derived from this software without
|
||||
# * specific prior written permission.
|
||||
# *
|
||||
# * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
|
||||
# * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
|
||||
# * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
|
||||
# * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
# * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
|
||||
# * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
# * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
|
||||
# * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
# * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||
# * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
|
||||
# * OF SUCH DAMAGE.
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
load_conf "fun"
|
||||
|
||||
# Common helpers, then the ones private to this module (helpers/fun.sh)
|
||||
# shellcheck source=../helpers/common.sh
|
||||
declare -F _profile_load_helpers >/dev/null || . "$(dirname -- "${BASH_SOURCE[0]}")/../helpers/common.sh"
|
||||
_profile_load_helpers "${BASH_SOURCE[0]}"
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# Make non-IT peoples think you're busy doing something hard: print the lines of
|
||||
# a hexdump of random data that contain a pattern, until a key is pressed.
|
||||
# Usage: busy [options] [pattern]
|
||||
# Options:
|
||||
# -p, --pattern <hex> : the string to search for in the hexdump output
|
||||
# (default is "ca fe"); may also be given as argument
|
||||
# -d, --delay <ms> : add a delay between each line output (milliseconds)
|
||||
busy()
|
||||
{
|
||||
local pattern="${BUSY_DEFAULT_PATTERN:-ca fe}" delay_ms="${BUSY_DEFAULT_DELAY:-0}"
|
||||
|
||||
local PARSED
|
||||
# Short: h, p:, d:
|
||||
# Long: help, pattern:, delay:
|
||||
PARSED=$(getopt -o hp:d: --long help,pattern:,delay: -n 'busy' -- "$@")
|
||||
# shellcheck disable=SC2181 # getopt return code is checked immediately after
|
||||
if [[ $? -ne 0 ]]; then
|
||||
disp E "Invalid options, use \"busy --help\" to display usage."
|
||||
return 1
|
||||
fi
|
||||
# Restore the getopt-normalised (shell-quoted) argument list
|
||||
local -a _pargs=()
|
||||
_getopt_split _pargs "$PARSED"
|
||||
set -- "${_pargs[@]}"
|
||||
|
||||
while true; do
|
||||
case "$1" in
|
||||
-h|--help)
|
||||
printf "busy: Monitor /dev/urandom for a specific pattern.\n\n"
|
||||
printf "Usage: busy [options] [pattern]\n\n"
|
||||
printf "Options:\n"
|
||||
printf "\t-h, --help\t\tDisplay this help screen\n"
|
||||
printf "\t-p, --pattern PATTERN\tHex pattern to search (default: \"ca fe\")\n"
|
||||
printf "\t-d, --delay MS\t\tDelay between matches in milliseconds\n\n"
|
||||
printf "It runs until a key is pressed.\n"
|
||||
return 0
|
||||
;;
|
||||
-p|--pattern)
|
||||
pattern="$2"
|
||||
shift 2
|
||||
;;
|
||||
-d|--delay)
|
||||
delay_ms="$2"
|
||||
shift 2
|
||||
;;
|
||||
--)
|
||||
shift
|
||||
break
|
||||
;;
|
||||
*)
|
||||
disp E "Invalid option: $1"
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
# A pattern given as argument (words joined by spaces) overrides -p
|
||||
[[ $# -gt 0 ]] && pattern="$*"
|
||||
|
||||
# Checked here so that the configured default is validated as well as -d
|
||||
if ! _is_uint "$delay_ms"; then
|
||||
disp E "Invalid delay '${delay_ms}': must be an integer (milliseconds)."
|
||||
return 1
|
||||
fi
|
||||
local delay_s
|
||||
delay_s=$(_fun_ms_to_s "$delay_ms")
|
||||
|
||||
command -v hexdump >/dev/null 2>&1 || {
|
||||
disp E "busy: 'hexdump' is required but not installed (util-linux or bsdmainutils)."
|
||||
return 1
|
||||
}
|
||||
_fun_need_tty busy || return 1
|
||||
|
||||
# Monitor /dev/urandom. grep -e protects a pattern starting with "-";
|
||||
# IFS= read keeps the line's leading blanks.
|
||||
(
|
||||
hexdump -C < /dev/urandom | grep -iF --line-buffered -e "$pattern" | \
|
||||
while IFS= read -r line; do
|
||||
printf '%s\n' "$line"
|
||||
[[ $delay_ms -gt 0 ]] && sleep "$delay_s"
|
||||
done
|
||||
) & local sub_pid=$!
|
||||
|
||||
_fun_stop_on_key "$sub_pid"
|
||||
}
|
||||
export -f busy
|
||||
# ------------------------------------------------------------------------------
|
||||
# Simulate a long and complex compilation process
|
||||
# Usage: fake_compile [options]
|
||||
# Options:
|
||||
# --min-delay=<ms> : minimum delay between output lines (milliseconds, default: 40)
|
||||
# --max-delay=<ms> : maximum delay between output lines (milliseconds, default: 150)
|
||||
# --lang=<lang> : source language preset: c (default), cpp, java, python, random
|
||||
# --errors : inject fake compilation errors at the end of the build
|
||||
fake_compile()
|
||||
{
|
||||
local min_ms="${FAKE_COMPILE_DEFAULT_MIN_DELAY:-40}" \
|
||||
max_ms="${FAKE_COMPILE_DEFAULT_MAX_DELAY:-150}" \
|
||||
lang="${FAKE_COMPILE_DEFAULT_LANG:-c}" with_errors=0
|
||||
|
||||
local PARSED
|
||||
# Short: h, n:, x:, l:, e
|
||||
# Long: help, min-delay:, max-delay:, lang:, errors
|
||||
PARSED=$(getopt -o hn:x:l:e --long help,min-delay:,max-delay:,lang:,errors -n 'fake_compile' -- "$@")
|
||||
# shellcheck disable=SC2181 # getopt return code is checked immediately after
|
||||
if [[ $? -ne 0 ]]; then
|
||||
disp E "Invalid options, use \"fake_compile --help\" to display usage."
|
||||
return 1
|
||||
fi
|
||||
# Restore the getopt-normalised (shell-quoted) argument list
|
||||
local -a _pargs=()
|
||||
_getopt_split _pargs "$PARSED"
|
||||
set -- "${_pargs[@]}"
|
||||
|
||||
while true; do
|
||||
case "$1" in
|
||||
-h|--help)
|
||||
printf "fake_compile: Simulate a complex compilation process.\n\n"
|
||||
printf "Usage: fake_compile [options]\n\n"
|
||||
printf "Options:\n"
|
||||
printf "\t-h, --help\t\t\tDisplay this help screen\n"
|
||||
printf "\t-n, --min-delay MS\t\tMin delay between lines in milliseconds (default: 40)\n"
|
||||
printf "\t-x, --max-delay MS\t\tMax delay between lines in milliseconds (default: 150)\n"
|
||||
printf "\t-l, --lang LANG\t\t\tLanguage preset: c, cpp, java, python, random (default: c)\n"
|
||||
printf "\t-e, --errors\t\t\tInject fake compilation errors at the end\n\n"
|
||||
printf "It runs until a key is pressed.\n"
|
||||
return 0
|
||||
;;
|
||||
-n|--min-delay)
|
||||
min_ms="$2"
|
||||
if ! [[ "$min_ms" =~ ^[0-9]+$ ]]; then
|
||||
disp E "Invalid min-delay: must be an integer (milliseconds)."
|
||||
return 1
|
||||
fi
|
||||
shift 2
|
||||
;;
|
||||
-x|--max-delay)
|
||||
max_ms="$2"
|
||||
if ! [[ "$max_ms" =~ ^[0-9]+$ ]]; then
|
||||
disp E "Invalid max-delay: must be an integer (milliseconds)."
|
||||
return 1
|
||||
fi
|
||||
shift 2
|
||||
;;
|
||||
-l|--lang)
|
||||
lang="$2"
|
||||
case "$lang" in
|
||||
c|cpp|java|python|random) ;;
|
||||
*)
|
||||
disp E "Invalid lang: must be one of: c, cpp, java, python, random."
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
shift 2
|
||||
;;
|
||||
-e|--errors)
|
||||
with_errors=1
|
||||
shift
|
||||
;;
|
||||
--)
|
||||
shift
|
||||
break
|
||||
;;
|
||||
*)
|
||||
disp E "Invalid option: $1"
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
# Validated here (not only in the options) so that the values coming from
|
||||
# the configuration file are checked too.
|
||||
if ! _is_uint "$min_ms" || ! _is_uint "$max_ms"; then
|
||||
disp E "min-delay and max-delay must be integers (milliseconds)."
|
||||
return 1
|
||||
fi
|
||||
case "$lang" in
|
||||
c|cpp|java|python|random) ;;
|
||||
*)
|
||||
disp E "Invalid lang '${lang}': must be one of: c, cpp, java, python, random."
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
if [[ $# -gt 0 ]]; then
|
||||
disp E "Unexpected argument: $1. Use \"fake_compile --help\" to display usage."
|
||||
return 1
|
||||
fi
|
||||
if [[ $min_ms -gt $max_ms ]]; then
|
||||
disp E "min-delay ($min_ms) must be <= max-delay ($max_ms)."
|
||||
return 1
|
||||
fi
|
||||
_fun_need_tty fake_compile || return 1
|
||||
|
||||
(
|
||||
# Sleep for a random duration between min_ms and max_ms (RANDOM only
|
||||
# spans 0..32767, which bounds the usable range).
|
||||
rand_sleep() {
|
||||
local r=$(( min_ms + RANDOM % (max_ms - min_ms + 1) ))
|
||||
sleep "$(_fun_ms_to_s "$r")"
|
||||
}
|
||||
|
||||
c_files=( "main" "utils" "parser" "lexer" "codegen" "optimizer"
|
||||
"allocator" "scheduler" "resolver" "runtime" "buffer"
|
||||
"hashmap" "io" "net" "crypto" "compress" )
|
||||
cpp_files=( "Application" "Controller" "AbstractFactory" "Singleton"
|
||||
"Observer" "Strategy" "Builder" "Facade" "Proxy"
|
||||
"Iterator" "Decorator" "CommandDispatcher" "EventLoop"
|
||||
"MemoryPool" "ThreadSafe" )
|
||||
java_files=( "Application" "Service" "Repository" "Controller" "Entity"
|
||||
"Configuration" "SecurityConfig" "DataSourceConfig"
|
||||
"RestTemplate" "ExceptionHandler" "Validator" )
|
||||
python_files=( "setup" "config" "utils" "models" "views" "serializers"
|
||||
"migrations/0001_initial" "migrations/0002_auto" "tests"
|
||||
"signals" "admin" "apps" "urls" "wsgi" "celery" "tasks" )
|
||||
|
||||
warnings=(
|
||||
"warning: implicit declaration of function"
|
||||
"warning: unused variable [-Wunused-variable]"
|
||||
"warning: comparison between signed and unsigned integer expressions"
|
||||
"warning: suggest parentheses around operand of '!'"
|
||||
"warning: format '%d' expects 'int', but argument has type 'long int'"
|
||||
"warning: control reaches end of non-void function [-Wreturn-type]"
|
||||
"warning: deprecated conversion from string literal to 'char*'"
|
||||
"warning: address of local variable taken"
|
||||
)
|
||||
errors=(
|
||||
"error: 'NULL' was not declared in this scope"
|
||||
"error: expected ';' before '}' token"
|
||||
"error: undefined reference to 'main'"
|
||||
"error: too few arguments to function"
|
||||
"error: invalid use of incomplete type"
|
||||
)
|
||||
|
||||
case "$lang" in
|
||||
c) files=("${c_files[@]}"); ext=".c" ;;
|
||||
cpp) files=("${cpp_files[@]}"); ext=".cpp" ;;
|
||||
java) files=("${java_files[@]}"); ext=".java" ;;
|
||||
python) files=("${python_files[@]}"); ext=".py" ;;
|
||||
esac
|
||||
|
||||
while true; do
|
||||
if [[ "$lang" == "random" ]]; then
|
||||
_langs=("c" "cpp" "java" "python")
|
||||
_pick="${_langs[$((RANDOM % 4))]}"
|
||||
case "$_pick" in
|
||||
c) files=("${c_files[@]}"); ext=".c" ;;
|
||||
cpp) files=("${cpp_files[@]}"); ext=".cpp" ;;
|
||||
java) files=("${java_files[@]}"); ext=".java" ;;
|
||||
python) files=("${python_files[@]}"); ext=".py" ;;
|
||||
esac
|
||||
fi
|
||||
total=${#files[@]}
|
||||
i=0
|
||||
for f in "${files[@]}"; do
|
||||
i=$(( i + 1 ))
|
||||
warn_count=$(( RANDOM % 3 ))
|
||||
printf "[ %2d/%2d ] Compiling %s%s ...\n" "$i" "$total" "$f" "$ext"
|
||||
rand_sleep
|
||||
w=0
|
||||
while [[ $w -lt $warn_count ]]; do
|
||||
wline="${warnings[$((RANDOM % ${#warnings[@]}))]}"
|
||||
printf " %s%s:%d:%d: %s\n" \
|
||||
"$f" "$ext" "$(( RANDOM % 200 + 1 ))" "$(( RANDOM % 80 + 1 ))" "$wline"
|
||||
rand_sleep
|
||||
w=$(( w + 1 ))
|
||||
done
|
||||
done
|
||||
|
||||
if [[ $with_errors -eq 1 ]]; then
|
||||
for eline in "${errors[@]}"; do
|
||||
ef="${files[$((RANDOM % ${#files[@]}))]}"
|
||||
printf " %s%s:%d:%d: %s\n" \
|
||||
"$ef" "$ext" "$(( RANDOM % 200 + 1 ))" "$(( RANDOM % 80 + 1 ))" "$eline"
|
||||
rand_sleep
|
||||
done
|
||||
printf "\nBuild FAILED: %d error(s), %d warning(s)\n" \
|
||||
"${#errors[@]}" "$(( RANDOM % 20 + 5 ))"
|
||||
else
|
||||
printf "\nBuild SUCCEEDED: 0 error(s), %d warning(s)\n" \
|
||||
"$(( RANDOM % 15 + 2 ))"
|
||||
fi
|
||||
printf "\n"
|
||||
done
|
||||
) & local sub_pid=$!
|
||||
|
||||
_fun_stop_on_key "$sub_pid"
|
||||
}
|
||||
export -f fake_compile
|
||||
# ------------------------------------------------------------------------------
|
||||
# Simulate a dramatic hacking sequence
|
||||
# Usage: hack [options]
|
||||
# Options:
|
||||
# --target=<ip> : target IP address (default: random)
|
||||
# --min-delay=<ms> : minimum delay between output lines (milliseconds, default: 60)
|
||||
# --max-delay=<ms> : maximum delay between output lines (milliseconds, default: 250)
|
||||
hack()
|
||||
{
|
||||
local min_ms="${HACK_DEFAULT_MIN_DELAY:-60}" \
|
||||
max_ms="${HACK_DEFAULT_MAX_DELAY:-250}" \
|
||||
target=""
|
||||
|
||||
local PARSED
|
||||
# Short: h, t:, n:, x:
|
||||
# Long: help, target:, min-delay:, max-delay:
|
||||
PARSED=$(getopt -o ht:n:x: --long help,target:,min-delay:,max-delay: -n 'hack' -- "$@")
|
||||
# shellcheck disable=SC2181 # getopt return code is checked immediately after
|
||||
if [[ $? -ne 0 ]]; then
|
||||
disp E "Invalid options, use \"hack --help\" to display usage."
|
||||
return 1
|
||||
fi
|
||||
# Restore the getopt-normalised (shell-quoted) argument list
|
||||
local -a _pargs=()
|
||||
_getopt_split _pargs "$PARSED"
|
||||
set -- "${_pargs[@]}"
|
||||
|
||||
while true; do
|
||||
case "$1" in
|
||||
-h|--help)
|
||||
printf "hack: Simulate a dramatic hacking sequence.\n\n"
|
||||
printf "Usage: hack [options]\n\n"
|
||||
printf "Options:\n"
|
||||
printf "\t-h, --help\t\t\tDisplay this help screen\n"
|
||||
printf "\t-t, --target IP\t\t\tTarget IP address (default: random)\n"
|
||||
printf "\t-n, --min-delay MS\t\tMin delay between output lines in milliseconds (default: 60)\n"
|
||||
printf "\t-x, --max-delay MS\t\tMax delay between output lines in milliseconds (default: 250)\n\n"
|
||||
printf "It runs until a key is pressed.\n"
|
||||
return 0
|
||||
;;
|
||||
-t|--target)
|
||||
target="$2"
|
||||
shift 2
|
||||
;;
|
||||
-n|--min-delay)
|
||||
min_ms="$2"
|
||||
if ! [[ "$min_ms" =~ ^[0-9]+$ ]]; then
|
||||
disp E "Invalid min-delay: must be an integer (milliseconds)."
|
||||
return 1
|
||||
fi
|
||||
shift 2
|
||||
;;
|
||||
-x|--max-delay)
|
||||
max_ms="$2"
|
||||
if ! [[ "$max_ms" =~ ^[0-9]+$ ]]; then
|
||||
disp E "Invalid max-delay: must be an integer (milliseconds)."
|
||||
return 1
|
||||
fi
|
||||
shift 2
|
||||
;;
|
||||
--)
|
||||
shift
|
||||
break
|
||||
;;
|
||||
*)
|
||||
disp E "Invalid option: $1"
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if ! _is_uint "$min_ms" || ! _is_uint "$max_ms"; then
|
||||
disp E "min-delay and max-delay must be integers (milliseconds)."
|
||||
return 1
|
||||
fi
|
||||
if [[ $# -gt 0 ]]; then
|
||||
disp E "Unexpected argument: $1. Use \"hack --help\" to display usage."
|
||||
return 1
|
||||
fi
|
||||
if [[ $min_ms -gt $max_ms ]]; then
|
||||
disp E "min-delay ($min_ms) must be <= max-delay ($max_ms)."
|
||||
return 1
|
||||
fi
|
||||
_fun_need_tty hack || return 1
|
||||
|
||||
(
|
||||
# Sleep for a random duration between min_ms and max_ms (RANDOM only
|
||||
# spans 0..32767, which bounds the usable range).
|
||||
rand_sleep() {
|
||||
local r=$(( min_ms + RANDOM % (max_ms - min_ms + 1) ))
|
||||
sleep "$(_fun_ms_to_s "$r")"
|
||||
}
|
||||
rand_ip() { printf '%d.%d.%d.%d\n' \
|
||||
$(( RANDOM%223+1 )) $(( RANDOM%255 )) \
|
||||
$(( RANDOM%255 )) $(( RANDOM%254+1 )); }
|
||||
rand_mac() { printf '%02x:%02x:%02x:%02x:%02x:%02x\n' \
|
||||
$(( RANDOM%256 )) $(( RANDOM%256 )) $(( RANDOM%256 )) \
|
||||
$(( RANDOM%256 )) $(( RANDOM%256 )) $(( RANDOM%256 )); }
|
||||
rand_hash() { printf '%04x%04x%04x%04x%04x%04x%04x%04x' \
|
||||
$RANDOM $RANDOM $RANDOM $RANDOM \
|
||||
$RANDOM $RANDOM $RANDOM $RANDOM; }
|
||||
|
||||
ports=( 22 80 443 3306 5432 6379 8080 8443 27017 )
|
||||
services=( "ssh" "http" "https" "mysql" "postgresql" "redis" "http-alt" "https-alt" "mongodb" )
|
||||
cve_ids=( "CVE-2024-3094" "CVE-2023-44487" "CVE-2024-6387" "CVE-2021-44228" "CVE-2022-0847" )
|
||||
os_list=( "Linux 5.15.x" "Linux 6.1.x" "Ubuntu 22.04 LTS" "Debian 12" "CentOS Stream 9" )
|
||||
users=( "root" "admin" "www-data" "postgres" "redis" "deploy" )
|
||||
passwords=( "password123" "admin2024" "letmein!" "Sup3rS3cr3t" "qwerty" "123456" )
|
||||
fixed_target="$target"
|
||||
|
||||
while true; do
|
||||
if [[ -z "$fixed_target" ]]; then
|
||||
target="$(rand_ip)"
|
||||
else
|
||||
target="$fixed_target"
|
||||
fi
|
||||
|
||||
printf "[*] Initializing attack sequence against %s\n" "$target"
|
||||
rand_sleep
|
||||
|
||||
# Phase 1 — port scan
|
||||
printf "[*] Starting port scan...\n"
|
||||
rand_sleep
|
||||
open_ports=()
|
||||
for idx in "${!ports[@]}"; do
|
||||
if (( RANDOM % 3 != 0 )); then
|
||||
printf " %-6s open %s\n" "${ports[$idx]}/tcp" "${services[$idx]}"
|
||||
open_ports+=( "${ports[$idx]}/${services[$idx]}" )
|
||||
rand_sleep
|
||||
fi
|
||||
done
|
||||
printf "[+] %d open port(s) found.\n" "${#open_ports[@]}"
|
||||
rand_sleep
|
||||
|
||||
# Phase 2 — OS fingerprinting
|
||||
printf "[*] OS fingerprinting...\n"
|
||||
rand_sleep
|
||||
printf "[+] Target OS: %s (MAC: %s)\n" \
|
||||
"${os_list[$((RANDOM % ${#os_list[@]}))]}" "$(rand_mac)"
|
||||
rand_sleep
|
||||
|
||||
# Phase 3 — CVE check
|
||||
printf "[*] Checking known vulnerabilities...\n"
|
||||
rand_sleep
|
||||
vuln_count=$(( RANDOM % 3 + 1 ))
|
||||
v=0
|
||||
while [[ $v -lt $vuln_count ]]; do
|
||||
printf "[!] Potential vulnerability: %s\n" "${cve_ids[$((RANDOM % ${#cve_ids[@]}))]}"
|
||||
rand_sleep
|
||||
v=$(( v + 1 ))
|
||||
done
|
||||
|
||||
# Phase 4 — exploit
|
||||
printf "[*] Loading exploit module...\n"; rand_sleep
|
||||
printf "[*] Bypassing firewall rules...\n"; rand_sleep
|
||||
printf "[*] Injecting payload"
|
||||
dots=0
|
||||
while [[ $dots -lt 6 ]]; do
|
||||
printf "."
|
||||
rand_sleep
|
||||
dots=$(( dots + 1 ))
|
||||
done
|
||||
printf "\n"
|
||||
printf "[+] Shell obtained on %s\n" "$target"
|
||||
rand_sleep
|
||||
|
||||
# Phase 5 — hash dumping
|
||||
printf "[*] Dumping password hashes...\n"
|
||||
rand_sleep
|
||||
for u in "${users[@]}"; do
|
||||
printf " %-12s : \$6\$%s\n" "$u" "$(rand_hash)"
|
||||
rand_sleep
|
||||
done
|
||||
|
||||
# Phase 6 — cracking
|
||||
printf "[*] Cracking hashes (wordlist: rockyou.txt)...\n"
|
||||
rand_sleep
|
||||
cracked=$(( RANDOM % ${#users[@]} + 1 ))
|
||||
c=0
|
||||
while [[ $c -lt $cracked ]]; do
|
||||
printf "[+] Cracked: %-12s -> %s\n" \
|
||||
"${users[$c]}" "${passwords[$((RANDOM % ${#passwords[@]}))]}"
|
||||
rand_sleep
|
||||
c=$(( c + 1 ))
|
||||
done
|
||||
|
||||
printf "\n[+] -------- ACCESS GRANTED -------- [+]\n"
|
||||
printf "[*] Cleaning logs on %s...\n" "$target"
|
||||
rand_sleep
|
||||
printf "[+] Done. Have a nice day.\n"
|
||||
printf "\n"
|
||||
done
|
||||
) & local sub_pid=$!
|
||||
|
||||
_fun_stop_on_key "$sub_pid"
|
||||
}
|
||||
export -f hack
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
# EOF
|
||||
@@ -0,0 +1,702 @@
|
||||
#!/usr/bin/env bash
|
||||
# ------------------------------------------------------------------------------
|
||||
# Copyright (c) 2013-2026 Geoffray Levasseur <fatalerrors@geoffray-levasseur.org>
|
||||
# Protected by the BSD3 license. Please read bellow for details.
|
||||
#
|
||||
# * Redistribution and use in source and binary forms,
|
||||
# * with or without modification, are permitted provided
|
||||
# * that the following conditions are met:
|
||||
# *
|
||||
# * Redistributions of source code must retain the above
|
||||
# * copyright notice, this list of conditions and the
|
||||
# * following disclaimer.
|
||||
# *
|
||||
# * Redistributions in binary form must reproduce the above
|
||||
# * copyright notice, this list of conditions and the following
|
||||
# * disclaimer in the documentation and/or other materials
|
||||
# * provided with the distribution.
|
||||
# *
|
||||
# * Neither the name of the copyright holder nor the names
|
||||
# * of any other contributors may be used to endorse or
|
||||
# * promote products derived from this software without
|
||||
# * specific prior written permission.
|
||||
# *
|
||||
# * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
|
||||
# * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
|
||||
# * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
|
||||
# * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
# * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
|
||||
# * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
# * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
|
||||
# * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
# * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||
# * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
|
||||
# * OF SUCH DAMAGE.
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
load_conf git
|
||||
|
||||
# Common helpers, then the ones private to this module (helpers/git.sh)
|
||||
# shellcheck source=../helpers/common.sh
|
||||
declare -F _profile_load_helpers >/dev/null || . "$(dirname -- "${BASH_SOURCE[0]}")/../helpers/common.sh"
|
||||
_profile_load_helpers "${BASH_SOURCE[0]}"
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# Built-in defaults (can be overridden from [git] section in profile.conf)
|
||||
: "${GIT_MAIN_BRANCH:=main}"
|
||||
: "${GIT_DEFAULT_REMOTE:=origin}"
|
||||
: "${GIT_WIP_PREFIX:=wip}"
|
||||
: "${GIT_GACP_AUTO_ADD:=1}"
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# Display compact git status + branch tracking information
|
||||
# Usage: gst [path]
|
||||
gst()
|
||||
{
|
||||
local PARSED
|
||||
PARSED=$(getopt -o h --long help -n 'gst' -- "$@")
|
||||
# shellcheck disable=SC2181 # getopt return code is checked immediately after
|
||||
if [[ $? -ne 0 ]]; then
|
||||
disp E "Invalid options, use \"gst --help\" to display usage."
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Restore the getopt-normalised (shell-quoted) argument list
|
||||
local -a _pargs=()
|
||||
_getopt_split _pargs "$PARSED"
|
||||
set -- "${_pargs[@]}"
|
||||
while true; do
|
||||
case "$1" in
|
||||
-h|--help)
|
||||
printf "gst: Display short git status and branch tracking info.\n"
|
||||
printf "Usage: gst [path]\n"
|
||||
return 0
|
||||
;;
|
||||
--)
|
||||
shift
|
||||
break
|
||||
;;
|
||||
*)
|
||||
disp E "Invalid options, use \"gst --help\" to display usage."
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
local target="${1:-.}"
|
||||
if ! command -v git >/dev/null 2>&1; then
|
||||
disp E "git command not found."
|
||||
return 1
|
||||
fi
|
||||
if ! git -C "$target" rev-parse --is-inside-work-tree >/dev/null 2>&1; then
|
||||
disp E "'$target' is not inside a git repository."
|
||||
return 1
|
||||
fi
|
||||
git -C "$target" status --short --branch
|
||||
}
|
||||
export -f gst
|
||||
# ------------------------------------------------------------------------------
|
||||
# Show a readable commit graph
|
||||
# Usage: ggraph [-n limit]
|
||||
ggraph()
|
||||
{
|
||||
local PARSED
|
||||
PARSED=$(getopt -o hn: --long help,limit: -n 'ggraph' -- "$@")
|
||||
# shellcheck disable=SC2181 # getopt return code is checked immediately after
|
||||
if [[ $? -ne 0 ]]; then
|
||||
disp E "Invalid options, use \"ggraph --help\" to display usage."
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Restore the getopt-normalised (shell-quoted) argument list
|
||||
local -a _pargs=()
|
||||
_getopt_split _pargs "$PARSED"
|
||||
set -- "${_pargs[@]}"
|
||||
local limit=30
|
||||
while true; do
|
||||
case "$1" in
|
||||
-h|--help)
|
||||
printf "ggraph: Display decorated git history graph.\n"
|
||||
printf "Usage: ggraph [-n limit]\n"
|
||||
printf "Options:\n"
|
||||
printf "\t-n, --limit\tNumber of commits to display (default: 30)\n"
|
||||
return 0
|
||||
;;
|
||||
-n|--limit)
|
||||
limit="$2"
|
||||
shift 2
|
||||
;;
|
||||
--)
|
||||
shift
|
||||
break
|
||||
;;
|
||||
*)
|
||||
disp E "Invalid options, use \"ggraph --help\" to display usage."
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
[[ $limit =~ ^[1-9][0-9]*$ ]] || {
|
||||
disp E "Invalid limit: must be a positive integer."
|
||||
return 1
|
||||
}
|
||||
|
||||
_git_require_repo || return 1
|
||||
git log --graph --decorate --oneline --all --max-count="$limit"
|
||||
}
|
||||
export -f ggraph
|
||||
# ------------------------------------------------------------------------------
|
||||
# Sync current branch with remote (fetch + rebase)
|
||||
# Usage: gsync [remote]
|
||||
gsync()
|
||||
{
|
||||
local PARSED
|
||||
PARSED=$(getopt -o h --long help -n 'gsync' -- "$@")
|
||||
# shellcheck disable=SC2181 # getopt return code is checked immediately after
|
||||
if [[ $? -ne 0 ]]; then
|
||||
disp E "Invalid options, use \"gsync --help\" to display usage."
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Restore the getopt-normalised (shell-quoted) argument list
|
||||
local -a _pargs=()
|
||||
_getopt_split _pargs "$PARSED"
|
||||
set -- "${_pargs[@]}"
|
||||
while true; do
|
||||
case "$1" in
|
||||
-h|--help)
|
||||
printf "gsync: Fetch and rebase current branch onto its remote tracking branch.\n"
|
||||
printf "Usage: gsync [remote]\n"
|
||||
return 0
|
||||
;;
|
||||
--)
|
||||
shift
|
||||
break
|
||||
;;
|
||||
*)
|
||||
disp E "Invalid options, use \"gsync --help\" to display usage."
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
_git_require_repo || return 1
|
||||
|
||||
local branch tracking remote upstream
|
||||
branch=$(_git_current_branch) || return 1
|
||||
upstream=$(git rev-parse --abbrev-ref --symbolic-full-name '@{u}' 2>/dev/null) || true
|
||||
|
||||
# Without argument, fetch the remote the branch actually tracks, so the
|
||||
# fetched refs and the rebase target always match; fall back to the
|
||||
# configured default remote.
|
||||
remote="${1:-}"
|
||||
if [[ -z $remote ]]; then
|
||||
if tracking=$(_git_tracking "$branch"); then
|
||||
remote="${tracking%%$'\t'*}"
|
||||
else
|
||||
remote="$GIT_DEFAULT_REMOTE"
|
||||
fi
|
||||
fi
|
||||
|
||||
disp I "Fetching from $remote..."
|
||||
git fetch --prune "$remote" || return 1
|
||||
|
||||
if [[ -z $upstream ]]; then
|
||||
disp W "No upstream configured for $branch, skipping rebase."
|
||||
disp I "Set one with: git branch --set-upstream-to ${remote}/${branch} ${branch}"
|
||||
return 0
|
||||
fi
|
||||
|
||||
disp I "Rebasing $branch onto $upstream..."
|
||||
git rebase "$upstream"
|
||||
}
|
||||
export -f gsync
|
||||
# ------------------------------------------------------------------------------
|
||||
# Add, commit, and push changes with automatic pull/rebase if needed
|
||||
# Usage: gacp -m "message" [file1 file2 ...]
|
||||
gacp()
|
||||
{
|
||||
local PARSED
|
||||
PARSED=$(getopt -o ham: --long help,auto,message: -n 'gacp' -- "$@")
|
||||
# shellcheck disable=SC2181 # getopt return code is checked immediately after
|
||||
if [[ $? -ne 0 ]]; then
|
||||
disp E "Invalid options, use \"gacp --help\" to display usage."
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Restore the getopt-normalised (shell-quoted) argument list
|
||||
local -a _pargs=()
|
||||
_getopt_split _pargs "$PARSED"
|
||||
set -- "${_pargs[@]}"
|
||||
local msg="" auto_add=0
|
||||
case "${GIT_GACP_AUTO_ADD,,}" in
|
||||
1|true|yes|on) auto_add=1 ;;
|
||||
esac
|
||||
while true; do
|
||||
case "$1" in
|
||||
-h|--help)
|
||||
printf "gacp: Run git add, git commit, and git push in one command.\n"
|
||||
printf "Usage: gacp [-a] -m \"message\" [file1 file2 ...]\n"
|
||||
printf "Options:\n"
|
||||
printf "\t-a, --auto\tAutomatically add all modified files (git add -A)\n"
|
||||
printf "\t-m, --message\tCommit message (mandatory)\n"
|
||||
printf "\n"
|
||||
printf "If files are provided, only those paths are added (-a is ignored).\n"
|
||||
printf "If no file is provided and -a is active, all changes are added with git add -A.\n"
|
||||
printf "Default for -a can be set via GIT_GACP_AUTO_ADD in profile.conf.\n"
|
||||
printf "If the remote branch moved forward, gacp pulls with rebase before pushing.\n"
|
||||
return 0
|
||||
;;
|
||||
-a|--auto)
|
||||
auto_add=1
|
||||
shift
|
||||
;;
|
||||
-m|--message)
|
||||
msg="$2"
|
||||
shift 2
|
||||
;;
|
||||
--)
|
||||
shift
|
||||
break
|
||||
;;
|
||||
*)
|
||||
disp E "Invalid options, use \"gacp --help\" to display usage."
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
_git_require_repo || return 1
|
||||
|
||||
if [[ -z $msg ]]; then
|
||||
disp E "Missing commit message. Use -m or --message."
|
||||
return 1
|
||||
fi
|
||||
|
||||
local branch upstream remote tracking_branch behind counts tracking
|
||||
branch=$(_git_current_branch) || return 1
|
||||
# Remote and branch tracked by the current branch (empty when none, or when
|
||||
# it tracks another local branch)
|
||||
if tracking=$(_git_tracking "$branch"); then
|
||||
remote="${tracking%%$'\t'*}"
|
||||
tracking_branch="${tracking#*$'\t'}"
|
||||
upstream="${remote}/${tracking_branch}"
|
||||
fi
|
||||
if [[ $# -gt 0 ]]; then
|
||||
auto_add=0
|
||||
disp I "Adding selected paths..."
|
||||
git add -- "$@" || return 1
|
||||
elif [[ $auto_add -eq 1 ]]; then
|
||||
disp I "Adding all changes..."
|
||||
git add -A || return 1
|
||||
else
|
||||
disp E "No files specified. Use -a/--auto or provide file paths."
|
||||
return 1
|
||||
fi
|
||||
|
||||
if git diff --cached --quiet; then
|
||||
disp W "No staged changes to commit."
|
||||
return 1
|
||||
fi
|
||||
|
||||
disp I "Creating commit..."
|
||||
git commit -m "$msg" || return 1
|
||||
|
||||
if [[ -z $upstream ]]; then
|
||||
remote="$GIT_DEFAULT_REMOTE"
|
||||
tracking_branch="$branch"
|
||||
fi
|
||||
|
||||
disp I "Fetching from $remote..."
|
||||
git fetch --prune "$remote" || return 1
|
||||
|
||||
if git rev-parse --verify --quiet "refs/remotes/${remote}/${tracking_branch}" >/dev/null; then
|
||||
counts=$(git rev-list --left-right --count HEAD..."${remote}/${tracking_branch}" 2>/dev/null) || return 1
|
||||
read -r _ behind <<< "$counts"
|
||||
if [[ ${behind:-0} -gt 0 ]]; then
|
||||
disp I "Remote branch is ahead, rebasing before push..."
|
||||
git pull --rebase "$remote" "$tracking_branch" || {
|
||||
disp E "Rebase failed. Your commit is kept locally: fix the conflicts (or 'git rebase --abort') and push manually."
|
||||
return 1
|
||||
}
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ -n $upstream ]]; then
|
||||
disp I "Pushing to $upstream..."
|
||||
git push || return 1
|
||||
else
|
||||
disp I "Pushing and setting upstream to ${remote}/${branch}..."
|
||||
git push -u "$remote" "$branch" || return 1
|
||||
fi
|
||||
|
||||
disp I "gacp complete."
|
||||
return 0
|
||||
}
|
||||
export -f gacp
|
||||
# ------------------------------------------------------------------------------
|
||||
# Reset local branch to exact upstream state (stash local changes first)
|
||||
# Usage: greset [target]
|
||||
greset()
|
||||
{
|
||||
local PARSED
|
||||
PARSED=$(getopt -o hx --long help,with-ignored -n 'greset' -- "$@")
|
||||
# shellcheck disable=SC2181 # getopt return code is checked immediately after
|
||||
if [[ $? -ne 0 ]]; then
|
||||
disp E "Invalid options, use \"greset --help\" to display usage."
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Restore the getopt-normalised (shell-quoted) argument list
|
||||
local -a _pargs=()
|
||||
_getopt_split _pargs "$PARSED"
|
||||
set -- "${_pargs[@]}"
|
||||
local clean_ignored=0
|
||||
while true; do
|
||||
case "$1" in
|
||||
-h|--help)
|
||||
printf "greset: Reset current branch to upstream, stashing local changes first.\n"
|
||||
printf "Usage: greset [target]\n"
|
||||
printf "Options:\n"
|
||||
printf "\t-x, --with-ignored\tAlso remove ignored files (git clean -fdx); they are\n"
|
||||
printf "\t\t\t\tnot stashed, so this cannot be undone\n"
|
||||
printf "\n"
|
||||
printf "Default target is current branch upstream (@{u}).\n"
|
||||
printf "If no upstream exists, fallback target is <remote>/<branch>.\n"
|
||||
printf "This command stashes local modifications (tracked + untracked),\n"
|
||||
printf "drops local unpushed commits by hard-reset, and cleans untracked files.\n"
|
||||
return 0
|
||||
;;
|
||||
-x|--with-ignored)
|
||||
clean_ignored=1
|
||||
shift
|
||||
;;
|
||||
--)
|
||||
shift
|
||||
break
|
||||
;;
|
||||
*)
|
||||
disp E "Invalid options, use \"greset --help\" to display usage."
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
_git_require_repo || return 1
|
||||
|
||||
local branch upstream target remote old_head stash_msg stash_out stash_created=0 dropped=0 root
|
||||
branch=$(git rev-parse --abbrev-ref HEAD 2>/dev/null) || return 1
|
||||
upstream=$(git rev-parse --abbrev-ref --symbolic-full-name '@{u}' 2>/dev/null) || true
|
||||
target="$1"
|
||||
|
||||
# A target starting with "-" would be taken for an option by git
|
||||
if [[ $target == -* ]]; then
|
||||
disp E "Invalid target '$target'."
|
||||
return 1
|
||||
fi
|
||||
|
||||
if [[ -z $target ]]; then
|
||||
if [[ -n $upstream ]]; then
|
||||
target="$upstream"
|
||||
elif [[ $branch == "HEAD" ]]; then
|
||||
disp E "HEAD is detached and has no upstream: give the target explicitly."
|
||||
return 1
|
||||
else
|
||||
remote="$GIT_DEFAULT_REMOTE"
|
||||
target="${remote}/${branch}"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ -z $remote ]]; then
|
||||
remote="${target%%/*}"
|
||||
fi
|
||||
|
||||
old_head=$(git rev-parse HEAD 2>/dev/null) || return 1
|
||||
|
||||
# Fetch and validate the target BEFORE touching the working tree: with an
|
||||
# unknown target, the function must fail without having stashed anything.
|
||||
# Only fetch when the derived remote is an actual configured remote; a bare
|
||||
# tag or local-branch target has no remote to update from.
|
||||
if git remote get-url "$remote" >/dev/null 2>&1; then
|
||||
disp I "Fetching from $remote..."
|
||||
git fetch --prune "$remote" || return 1
|
||||
else
|
||||
disp I "Target '$target' is not a remote ref; skipping fetch."
|
||||
fi
|
||||
|
||||
if ! git rev-parse --verify --quiet "${target}^{commit}" >/dev/null; then
|
||||
disp E "Target '$target' does not exist."
|
||||
return 1
|
||||
fi
|
||||
|
||||
# "status --porcelain" covers the whole repository (untracked files
|
||||
# included), whatever the current subdirectory is.
|
||||
if [[ -n $(git status --porcelain 2>/dev/null) ]]; then
|
||||
stash_msg="greset:${branch}:$(date +'%Y-%m-%d %H:%M:%S')"
|
||||
disp I "Stashing local changes as '$stash_msg'..."
|
||||
local stash_before stash_after
|
||||
stash_before=$(git rev-parse --quiet --verify refs/stash 2>/dev/null) || true
|
||||
stash_out=$(git stash push -u -m "$stash_msg" 2>&1) || {
|
||||
disp E "Failed to stash local changes."
|
||||
printf "%s\n" "$stash_out"
|
||||
return 1
|
||||
}
|
||||
stash_after=$(git rev-parse --quiet --verify refs/stash 2>/dev/null) || true
|
||||
# A new stash means the top refs/stash sha changed (locale-independent).
|
||||
if [[ -n $stash_after && $stash_after != "$stash_before" ]]; then
|
||||
stash_created=1
|
||||
fi
|
||||
fi
|
||||
|
||||
dropped=$(git rev-list --count "${target}..${old_head}" 2>/dev/null || printf "0")
|
||||
|
||||
disp W "Hard-resetting $branch to $target..."
|
||||
git reset --hard "$target" || return 1
|
||||
|
||||
# git clean only works below the current directory: run it from the root so
|
||||
# it covers the same tree as the reset and the stash above.
|
||||
root=$(git rev-parse --show-toplevel) || return 1
|
||||
if (( clean_ignored )); then
|
||||
git -C "$root" clean -fdx || return 1
|
||||
else
|
||||
git -C "$root" clean -fd || return 1
|
||||
fi
|
||||
|
||||
if (( stash_created )); then
|
||||
disp I "Local changes were stashed. Use 'git stash list' and 'git stash pop' when needed."
|
||||
fi
|
||||
disp I "greset complete. Dropped local-only commits: $dropped"
|
||||
disp I "Previous HEAD was ${old_head:0:12}; 'git reset --hard ${old_head:0:12}' restores it."
|
||||
return 0
|
||||
}
|
||||
export -f greset
|
||||
# ------------------------------------------------------------------------------
|
||||
# Create a quick WIP commit for local checkpointing
|
||||
# Usage: gwip [message]
|
||||
gwip()
|
||||
{
|
||||
local PARSED
|
||||
PARSED=$(getopt -o h --long help -n 'gwip' -- "$@")
|
||||
# shellcheck disable=SC2181 # getopt return code is checked immediately after
|
||||
if [[ $? -ne 0 ]]; then
|
||||
disp E "Invalid options, use \"gwip --help\" to display usage."
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Restore the getopt-normalised (shell-quoted) argument list
|
||||
local -a _pargs=()
|
||||
_getopt_split _pargs "$PARSED"
|
||||
set -- "${_pargs[@]}"
|
||||
while true; do
|
||||
case "$1" in
|
||||
-h|--help)
|
||||
printf "gwip: Create a local checkpoint commit with all tracked/untracked changes.\n"
|
||||
printf "Usage: gwip [message]\n"
|
||||
return 0
|
||||
;;
|
||||
--)
|
||||
shift
|
||||
break
|
||||
;;
|
||||
*)
|
||||
disp E "Invalid options, use \"gwip --help\" to display usage."
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
_git_require_repo || return 1
|
||||
|
||||
local msg
|
||||
if [[ $# -gt 0 ]]; then
|
||||
msg="$*"
|
||||
else
|
||||
msg="$GIT_WIP_PREFIX: $(date +'%Y-%m-%d %H:%M:%S')"
|
||||
fi
|
||||
|
||||
git add -A || return 1
|
||||
git commit -m "$msg"
|
||||
}
|
||||
export -f gwip
|
||||
# ------------------------------------------------------------------------------
|
||||
# Delete merged local branches (except protected branches)
|
||||
# Usage: gprune [-D|--deep] [main-branch]
|
||||
#
|
||||
# Default mode: deletes branches already merged into <main-branch> locally
|
||||
# (git branch --merged).
|
||||
# Deep mode (-D/--deep): additionally deletes branches whose tracking remote
|
||||
# has disappeared (remote pruned after a merge request / pull request merge).
|
||||
# Those branches are detected via 'git fetch --prune' + remote-tracking gone.
|
||||
# This is the common case when the MR was merged upstream and the remote
|
||||
# branch was deleted by the forge. Deletion uses 'git branch -D' (force)
|
||||
# because the local branch has no merged ancestor that git can verify locally.
|
||||
gprune()
|
||||
{
|
||||
local PARSED deep=0
|
||||
PARSED=$(getopt -o hD --long help,deep -n 'gprune' -- "$@")
|
||||
# shellcheck disable=SC2181 # getopt return code is checked immediately after
|
||||
if [[ $? -ne 0 ]]; then
|
||||
disp E "Invalid options, use \"gprune --help\" to display usage."
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Restore the getopt-normalised (shell-quoted) argument list
|
||||
local -a _pargs=()
|
||||
_getopt_split _pargs "$PARSED"
|
||||
set -- "${_pargs[@]}"
|
||||
while true; do
|
||||
case "$1" in
|
||||
-h|--help)
|
||||
printf "gprune: Delete local branches already merged into main branch.\n\n"
|
||||
printf "Usage: gprune [-D|--deep] [main-branch]\n\n"
|
||||
printf "Options:\n"
|
||||
printf "\t-D, --deep\tAlso delete branches whose upstream was removed\n"
|
||||
printf "\t\t\t(remote deleted after MR/PR merge). Uses 'git branch -D'.\n"
|
||||
printf "\t-h, --help\tDisplay this help screen\n\n"
|
||||
printf "Arguments:\n"
|
||||
printf "\tmain-branch\tBase branch to check merges against (default: auto-detected)\n"
|
||||
return 0
|
||||
;;
|
||||
-D|--deep)
|
||||
deep=1
|
||||
shift
|
||||
;;
|
||||
--)
|
||||
shift
|
||||
break
|
||||
;;
|
||||
*)
|
||||
disp E "Invalid options, use \"gprune --help\" to display usage."
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
_git_require_repo || return 1
|
||||
|
||||
local base="${1:-$(_git_default_branch "$GIT_DEFAULT_REMOTE")}" current deleted=0 b
|
||||
current=$(git rev-parse --abbrev-ref HEAD 2>/dev/null) || return 1
|
||||
|
||||
if ! git rev-parse --verify --quiet "refs/heads/${base}" >/dev/null; then
|
||||
disp E "Base branch '$base' does not exist locally."
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Branches that are never deleted: the base, the checked-out one, the usual
|
||||
# trunk names and the configured main branch.
|
||||
local -a protected=("$base" "$current" master main develop dev "$GIT_MAIN_BRANCH")
|
||||
_gprune_is_protected()
|
||||
{
|
||||
local p
|
||||
for p in "${protected[@]}"; do
|
||||
[[ $1 == "$p" ]] && return 0
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
# ── Standard mode: branches locally merged into base ──────────────────────
|
||||
disp I "Pruning branches merged into $base..."
|
||||
|
||||
# for-each-ref gives plain names (git branch output carries "*", "+" and
|
||||
# colour markers); "-d" refuses anything not fully merged.
|
||||
while IFS= read -r b; do
|
||||
[[ -z $b ]] && continue
|
||||
_gprune_is_protected "$b" && continue
|
||||
if git branch -d "$b" >/dev/null 2>&1; then
|
||||
printf "Deleted (merged): %s\n" "$b"
|
||||
deleted=$(( deleted + 1 ))
|
||||
fi
|
||||
done < <(git for-each-ref --merged="$base" --format='%(refname:short)' refs/heads/)
|
||||
|
||||
# ── Deep mode: branches whose remote tracking ref was deleted upstream ─────
|
||||
if (( deep )); then
|
||||
disp I "Deep mode: pruning remote-tracking refs, then checking for gone branches..."
|
||||
git fetch --prune --quiet || disp W "Fetch failed: using the remote-tracking refs as they are."
|
||||
|
||||
local upstream sha
|
||||
# %(upstream) is the full ref configured as upstream, and stays set once
|
||||
# the remote branch is gone, whereas "git rev-parse <branch>@{upstream}"
|
||||
# fails then. It also avoids parsing the localized "[gone]" marker.
|
||||
while IFS=$'\t' read -r b upstream; do
|
||||
[[ -z $b ]] && continue
|
||||
_gprune_is_protected "$b" && continue
|
||||
[[ -z $upstream ]] && continue # no tracking branch at all — skip
|
||||
|
||||
# If the upstream ref still exists, it is not gone.
|
||||
git show-ref --verify --quiet "$upstream" 2>/dev/null && continue
|
||||
sha=$(git rev-parse --short "$b" 2>/dev/null)
|
||||
if git branch -D "$b" >/dev/null 2>&1; then
|
||||
printf "Deleted (gone upstream): %s (%s)\n" "$b" "$sha"
|
||||
deleted=$(( deleted + 1 ))
|
||||
fi
|
||||
done < <(git for-each-ref --format='%(refname:short)%09%(upstream)' refs/heads/)
|
||||
fi
|
||||
|
||||
unset -f _gprune_is_protected
|
||||
# "if" keeps the exit status at 0 when branches were deleted
|
||||
if (( deleted == 0 )); then
|
||||
disp I "No branches to delete."
|
||||
fi
|
||||
}
|
||||
export -f gprune
|
||||
# ------------------------------------------------------------------------------
|
||||
# Print repository root path
|
||||
# Usage: groot
|
||||
groot()
|
||||
{
|
||||
local PARSED
|
||||
PARSED=$(getopt -o hg --long help,go -n 'groot' -- "$@")
|
||||
# shellcheck disable=SC2181 # getopt return code is checked immediately after
|
||||
if [[ $? -ne 0 ]]; then
|
||||
disp E "Invalid options, use \"groot --help\" to display usage."
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Restore the getopt-normalised (shell-quoted) argument list
|
||||
local -a _pargs=()
|
||||
_getopt_split _pargs "$PARSED"
|
||||
set -- "${_pargs[@]}"
|
||||
local do_go=0
|
||||
while true; do
|
||||
case "$1" in
|
||||
-h|--help)
|
||||
printf "groot: Display the absolute path of the current repository root.\n"
|
||||
printf "Usage: groot [-g|--go]\n"
|
||||
printf "Options:\n"
|
||||
printf "\t-g, --go\tChange current directory to repository root\n"
|
||||
return 0
|
||||
;;
|
||||
-g|--go)
|
||||
do_go=1
|
||||
shift
|
||||
;;
|
||||
--)
|
||||
shift
|
||||
break
|
||||
;;
|
||||
*)
|
||||
disp E "Invalid options, use \"groot --help\" to display usage."
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
_git_require_repo || return 1
|
||||
local root
|
||||
root=$(git rev-parse --show-toplevel) || return 1
|
||||
|
||||
if (( do_go )); then
|
||||
cd "$root" || {
|
||||
disp E "Failed to move to repository root: $root"
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
fi
|
||||
|
||||
printf "%s\n" "$root"
|
||||
}
|
||||
export -f groot
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
# EOF
|
||||
+139
-25
@@ -1,33 +1,147 @@
|
||||
#!/usr/bin/env bash
|
||||
# ------------------------------------------------------------------------------
|
||||
# Copyright (c) 2013-2026 Geoffray Levasseur <fatalerrors@geoffray-levasseur.org>
|
||||
# Protected by the BSD3 license. Please read bellow for details.
|
||||
#
|
||||
# * Redistribution and use in source and binary forms,
|
||||
# * with or without modification, are permitted provided
|
||||
# * that the following conditions are met:
|
||||
# *
|
||||
# * Redistributions of source code must retain the above
|
||||
# * copyright notice, this list of conditions and the
|
||||
# * following disclaimer.
|
||||
# *
|
||||
# * Redistributions in binary form must reproduce the above
|
||||
# * copyright notice, this list of conditions and the following
|
||||
# * disclaimer in the documentation and/or other materials
|
||||
# * provided with the distribution.
|
||||
# *
|
||||
# * Neither the name of the copyright holder nor the names
|
||||
# * of any other contributors may be used to endorse or
|
||||
# * promote products derived from this software without
|
||||
# * specific prior written permission.
|
||||
# *
|
||||
# * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
|
||||
# * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
|
||||
# * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
|
||||
# * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
# * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
|
||||
# * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
# * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
|
||||
# * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
# * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||
# * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
|
||||
# * OF SUCH DAMAGE.
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
load_conf "help"
|
||||
|
||||
# Common helpers, then the ones private to this module (helpers/help.sh)
|
||||
# shellcheck source=../helpers/common.sh
|
||||
declare -F _profile_load_helpers >/dev/null || . "$(dirname -- "${BASH_SOURCE[0]}")/../helpers/common.sh"
|
||||
_profile_load_helpers "${BASH_SOURCE[0]}"
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# Display list of commands and general informations
|
||||
# ------------------------------------------------------------------------------
|
||||
# Usage: help [command]
|
||||
help()
|
||||
{
|
||||
cat <<EOF
|
||||
clean Erase backup files
|
||||
dpkgs Search for the given package in the installed ones
|
||||
gpid Give the list of PIDs for the given process name
|
||||
isipv4 Tell if the given IPv4 is valid
|
||||
isipv6 Tell if the given IPv6 is valid
|
||||
ku Kill process owned by users in parameter
|
||||
mcd Create a directory and go inside
|
||||
meteo Display curent weather forecast for the configured city
|
||||
ppg Display process matching the given parameter
|
||||
rain Let the rain fall
|
||||
rmhost Remove host (IP and/or DNS name) for current known_host
|
||||
rmspc Remove spaces from all the files in working directory
|
||||
setc Set console language to C
|
||||
setfr Set console language to French
|
||||
settrace Activate/deactivate call trace for script debugging
|
||||
setus Set console language to US English
|
||||
showinfo Show the welcoming baner with basic system information
|
||||
ssr Do a root login to the given address
|
||||
taz Compress smartly the given files or directory
|
||||
utaz Uncompress all zip files in the given (or current) directory
|
||||
ver Display version of your copy of profile
|
||||
# With a command name, delegate to its own --help output. Only shell
|
||||
# functions are accepted: running an arbitrary external program with an
|
||||
# option it may not understand could have side effects.
|
||||
if [[ $# -gt 0 && "$1" != "--help" && "$1" != "-h" ]]; then
|
||||
local cmd="$1"
|
||||
if [[ $cmd == -* ]]; then
|
||||
disp E "Invalid option: $cmd. Usage: help [command]"
|
||||
return 1
|
||||
elif declare -F -- "$cmd" >/dev/null 2>&1; then
|
||||
"$cmd" --help
|
||||
elif command -v -- "$cmd" >/dev/null 2>&1; then
|
||||
disp E "'$cmd' is not a profile command: try \"$cmd --help\" or \"man $cmd\"."
|
||||
return 1
|
||||
else
|
||||
disp E "Unknown command: $cmd"
|
||||
return 1
|
||||
fi
|
||||
return
|
||||
fi
|
||||
|
||||
Please use <command> --help to obtain usage details.
|
||||
EOF
|
||||
# Bold title, without colour codes when NO_COLOR is set (like disp does)
|
||||
if [[ -n ${NO_COLOR:-} ]]; then
|
||||
printf "Welcome to your profile! Here is a list of available commands:\n\n"
|
||||
else
|
||||
# shellcheck disable=SC2154 # colour variables are defined by disp.sh
|
||||
printf "%b%s%b\n\n" "$BIWhite" \
|
||||
"Welcome to your profile! Here is a list of available commands:" "$DEFAULTCOL"
|
||||
fi
|
||||
printf "backtrace\tPrint the current call stack\n"
|
||||
printf "busy\t\tMonitor /dev/urandom for a hex pattern — look busy\n"
|
||||
printf "check_updates\tCheck for new versions of profile\n"
|
||||
printf "clean\t\tErase backup files in given directories, optionally recursive\n"
|
||||
printf "conf_dump\tDisplay the profile configuration file\n"
|
||||
printf "conf_save\tSave or update a key=value pair in a profile configuration section\n"
|
||||
printf "disp\t\tDisplay formatted info/warning/error/debug messages\n"
|
||||
printf "dwl\t\tDownload a URL using curl, wget, or fetch transparently\n"
|
||||
printf "expandlist\tExpand glob expressions into a quoted, separated list\n"
|
||||
printf "fake_compile\tSimulate a long compilation process — look busy\n"
|
||||
printf "file_stats\tDisplay file size statistics for a path\n"
|
||||
printf "findbig\t\tFind the biggest files in the given or current directory\n"
|
||||
printf "finddead\tFind dead symbolic links in the given or current directory\n"
|
||||
printf "findzero\tFind empty files in the given or current directory\n"
|
||||
printf "gacp\t\tAdd, commit and push changes (auto-pull if needed)\n"
|
||||
printf "genpwd\t\tGenerate one or more random secure passwords with configurable constraints\n"
|
||||
printf "get_pkgmgr\tDetect the package manager of the running distribution\n"
|
||||
printf "ggraph\t\tDisplay decorated git history graph\n"
|
||||
printf "gpid\t\tGive the list of PIDs matching the given process name(s)\n"
|
||||
printf "gprune\t\tDelete local branches already merged, or after remote deletion (MR / PR)\n"
|
||||
printf "greset\t\tReset branch to upstream (stash local, drop local commits)\n"
|
||||
printf "groot\t\tDisplay repository root path (or cd to it with -g)\n"
|
||||
printf "gsync\t\tFetch and rebase current branch onto upstream\n"
|
||||
printf "gst\t\tDisplay short git status and branch tracking info\n"
|
||||
printf "gwip\t\tCreate a quick WIP checkpoint commit\n"
|
||||
printf "hack\t\tSimulate a dramatic hacking sequence — look dangerous\n"
|
||||
printf "help\t\tDisplay this list, or the usage of a profile command (help <command>)\n"
|
||||
printf "isipv4\t\tTell if the given parameter is a valid IPv4 address\n"
|
||||
printf "isipv6\t\tTell if the given parameter is a valid IPv6 address\n"
|
||||
printf "kt\t\tKill a process and all its children (kill tree)\n"
|
||||
printf "ku\t\tKill all processes owned by the given user name or ID\n"
|
||||
printf "locale_check\tTell whether a locale is installed (canonical name on stdout)\n"
|
||||
printf "matrix\t\tConsole screensaver with Matrix-style digital rain (binary, kana, ascii charset)\n"
|
||||
printf "mcd\t\tCreate a directory and immediately move into it\n"
|
||||
printf "mdcat\t\tRender Markdown files in terminal with colors, tables and code frames\n"
|
||||
printf "meteo\t\tDisplay weather forecast for the configured or given city\n"
|
||||
printf "myextip\t\tGet information about your public IP address\n"
|
||||
printf "pinfo\t\tDisplay system information read from /proc and /sys only\n"
|
||||
printf "pkgf\t\tFind which installed package owns a given file (distro-aware)\n"
|
||||
printf "pkgi\t\tInstall packages (distro-aware, sudo/doas when needed)\n"
|
||||
printf "pkgr\t\tUninstall packages; -p purges configuration, -d removes unneeded dependencies\n"
|
||||
printf "pkgs\t\tSearch for a pattern in installed package names (distro-aware)\n"
|
||||
printf "ppg\t\tLook for the given pattern in running processes\n"
|
||||
printf "ppn\t\tList processes matching an exact command name\n"
|
||||
printf "ppu\t\tList processes owned by a specific user\n"
|
||||
printf "profile_upgrade\tUpgrade profile to the latest version (git pull or archive)\n"
|
||||
printf "pwdscore\tCalculate the strength score of a given password\n"
|
||||
printf "rain\t\tConsole screensaver with falling-rain effect (multiple color themes)\n"
|
||||
printf "rainbow\t\tFull-screen rainbow screensaver using background colors only\n"
|
||||
printf "rmhost\t\tRemove host (name and IP) from SSH known_hosts; supports --all-users as root\n"
|
||||
printf "rmspc\t\tReplace spaces in filenames with underscores (or a custom character)\n"
|
||||
printf "setlocale\tSet console locale to any installed locale\n"
|
||||
printf " * setc\tSet locale to standard C (POSIX)\n"
|
||||
printf " * set*\tLocale shortcuts generated from SET_LOCALE in profile.conf\n"
|
||||
printf "set_theme\tSwitch the prompt colour theme; no argument lists available themes\n"
|
||||
printf "settrace\tActivate or deactivate ERR trap to display backtrace on script errors\n"
|
||||
printf "showinfo\tDisplay welcome banner and system information (figlet + fastfetch/neofetch)\n"
|
||||
printf "ssr\t\tSSH into a server as root, forwarding extra ssh options\n"
|
||||
printf "taz\t\tCompress files and directories into a chosen archive format\n"
|
||||
printf "term_set\tSet TERM to the best available terminal capability (auto-detect or honour config)\n"
|
||||
printf "urlencode\tURL-encode a string\n"
|
||||
printf "utaz\t\tSmartly uncompress archives (zip, tar.gz/bz2/xz/lz, rar, arj, lha, ace, 7z, zst, cpio, cab, deb, rpm)\n"
|
||||
printf "ver\t\tDisplay the installed profile version\n"
|
||||
|
||||
printf "\nPlease use <command> --help or help <command> to obtain usage details.\n"
|
||||
}
|
||||
export -f help
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
|
||||
# EOF
|
||||
|
||||
+433
-31
@@ -1,55 +1,457 @@
|
||||
#!/usr/bin/env bash
|
||||
# ------------------------------------------------------------------------------
|
||||
# Copyright (c) 2013-2026 Geoffray Levasseur <fatalerrors@geoffray-levasseur.org>
|
||||
# Protected by the BSD3 license. Please read bellow for details.
|
||||
#
|
||||
# * Redistribution and use in source and binary forms,
|
||||
# * with or without modification, are permitted provided
|
||||
# * that the following conditions are met:
|
||||
# *
|
||||
# * Redistributions of source code must retain the above
|
||||
# * copyright notice, this list of conditions and the
|
||||
# * following disclaimer.
|
||||
# *
|
||||
# * Redistributions in binary form must reproduce the above
|
||||
# * copyright notice, this list of conditions and the following
|
||||
# * disclaimer in the documentation and/or other materials
|
||||
# * provided with the distribution.
|
||||
# *
|
||||
# * Neither the name of the copyright holder nor the names
|
||||
# * of any other contributors may be used to endorse or
|
||||
# * promote products derived from this software without
|
||||
# * specific prior written permission.
|
||||
# *
|
||||
# * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
|
||||
# * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
|
||||
# * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
|
||||
# * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
# * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
|
||||
# * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
# * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
|
||||
# * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
# * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||
# * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
|
||||
# * OF SUCH DAMAGE.
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
# Common helpers (helpers/common.sh)
|
||||
# shellcheck source=../helpers/common.sh
|
||||
declare -F _profile_load_helpers >/dev/null || . "$(dirname -- "${BASH_SOURCE[0]}")/../helpers/common.sh"
|
||||
|
||||
load_conf info
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# Show profile version
|
||||
# ------------------------------------------------------------------------------
|
||||
ver ()
|
||||
# Usage: ver
|
||||
ver()
|
||||
{
|
||||
echo "Profile version $PROFVERSION."
|
||||
local PARSED
|
||||
|
||||
PARSED=$(getopt -o h --long help -n 'ver' -- "$@")
|
||||
# shellcheck disable=SC2181 # getopt return code is checked immediately after
|
||||
if [[ $? -ne 0 ]]; then
|
||||
disp E "Invalid options, use \"ver --help\" to display usage."
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Restore the getopt-normalised (shell-quoted) argument list
|
||||
local -a _pargs=()
|
||||
_getopt_split _pargs "$PARSED"
|
||||
set -- "${_pargs[@]}"
|
||||
while true; do
|
||||
case "$1" in
|
||||
-h|--help)
|
||||
printf "ver: Display the current profile version.\nUsage: ver\n"
|
||||
return 0
|
||||
;;
|
||||
--)
|
||||
shift
|
||||
break
|
||||
;;
|
||||
*)
|
||||
disp E "Invalid options, use \"ver --help\" to display usage."
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
[[ -z ${PROFVERSION:-} ]] && \
|
||||
disp W "No version defined. Profile is probably badly installed." && \
|
||||
return 1
|
||||
disp "Profile version $PROFVERSION."
|
||||
}
|
||||
export -f ver
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# Display weather of the given city (or default one)
|
||||
# ------------------------------------------------------------------------------
|
||||
meteo ()
|
||||
# Display weather for the given city (or the default one)
|
||||
# Usage: meteo [city1 city2 ...]
|
||||
meteo()
|
||||
{
|
||||
cities=$@
|
||||
[[ $# -eq 0 ]] && local cities=$DEFAULT_CITY
|
||||
local PARSED
|
||||
|
||||
for city in $cities; do
|
||||
curl https://wttr.in/$city || echo "Failed fetching datas for $city."
|
||||
PARSED=$(getopt -o h --long help -n 'meteo' -- "$@")
|
||||
# shellcheck disable=SC2181 # getopt return code is checked immediately after
|
||||
if [[ $? -ne 0 ]]; then
|
||||
disp E "Invalid options, use \"meteo --help\" to display usage."
|
||||
return 1
|
||||
fi
|
||||
# Restore the getopt-normalised (shell-quoted) argument list
|
||||
local -a _pargs=()
|
||||
_getopt_split _pargs "$PARSED"
|
||||
set -- "${_pargs[@]}"
|
||||
while true; do
|
||||
case "$1" in
|
||||
-h|--help)
|
||||
printf "meteo: Fetch weather data.\n"
|
||||
printf "Usage: meteo [city1 city2 ...]\n"
|
||||
printf "If no city is provided, the default city from configuration will be used.\n"
|
||||
return 0
|
||||
;;
|
||||
--)
|
||||
shift
|
||||
break
|
||||
;;
|
||||
*)
|
||||
disp E "Invalid options, use \"meteo --help\" to display usage."
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
local cities=("$@")
|
||||
local city="" encoded=""
|
||||
[[ $# -eq 0 ]] && cities=("${METEO_DEFAULT_CITY:-}")
|
||||
|
||||
if [[ ${#cities[@]} -eq 0 || -z "${cities[0]}" ]]; then
|
||||
disp E "No city given and METEO_DEFAULT_CITY is not set. Use 'meteo <city>'."
|
||||
return 1
|
||||
fi
|
||||
|
||||
for city in "${cities[@]}"; do
|
||||
encoded=$(urlencode "$city")
|
||||
dwl "https://wttr.in/$encoded" || \
|
||||
disp E "Failed to fetch weather data for $city."
|
||||
done
|
||||
}
|
||||
export -f meteo
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# Display a system information block using only /proc and /sys — no external tools.
|
||||
# Usage: pinfo
|
||||
pinfo()
|
||||
{
|
||||
local _row
|
||||
_row() {
|
||||
printf " %b%-12s%b %b%s%b\n" "$_lbl" "$1" "$_rst" "$_val" "$2" "$_rst"
|
||||
}
|
||||
|
||||
local PARSED
|
||||
PARSED=$(getopt -o h --long help -n 'pinfo' -- "$@")
|
||||
# shellcheck disable=SC2181
|
||||
if [[ $? -ne 0 ]]; then
|
||||
disp E "Invalid options, use \"pinfo --help\" to display usage."
|
||||
return 1
|
||||
fi
|
||||
# Restore the getopt-normalised (shell-quoted) argument list
|
||||
local -a _pargs=()
|
||||
_getopt_split _pargs "$PARSED"
|
||||
set -- "${_pargs[@]}"
|
||||
while true; do
|
||||
case "$1" in
|
||||
-h|--help)
|
||||
printf "pinfo: Display system information from /proc and /sys (no external tools required).\n"
|
||||
printf "Usage: pinfo\n"
|
||||
return 0
|
||||
;;
|
||||
--)
|
||||
shift
|
||||
break
|
||||
;;
|
||||
*)
|
||||
disp E "Invalid options, use \"pinfo --help\" to display usage."
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
# --- Hostname ---
|
||||
local hostname_str
|
||||
if [[ -r /proc/sys/kernel/hostname ]]; then
|
||||
read -r hostname_str < /proc/sys/kernel/hostname
|
||||
else
|
||||
hostname_str="${HOSTNAME:-unknown}"
|
||||
fi
|
||||
|
||||
# --- OS release (security: parsed line by line, never sourced) ---
|
||||
# /usr/lib/os-release is the documented fallback location. Values may be
|
||||
# wrapped in double or single quotes.
|
||||
local os_name="Unknown" os_version="" _osr_file=""
|
||||
if [[ -r /etc/os-release ]]; then
|
||||
_osr_file=/etc/os-release
|
||||
elif [[ -r /usr/lib/os-release ]]; then
|
||||
_osr_file=/usr/lib/os-release
|
||||
fi
|
||||
if [[ -n $_osr_file ]]; then
|
||||
local _osr_key _osr_val
|
||||
while IFS='=' read -r _osr_key _osr_val; do
|
||||
_osr_val="${_osr_val//\"/}"
|
||||
_osr_val="${_osr_val//\'/}"
|
||||
case "$_osr_key" in
|
||||
NAME) os_name="$_osr_val" ;;
|
||||
VERSION_ID) os_version="$_osr_val" ;;
|
||||
esac
|
||||
done < "$_osr_file"
|
||||
fi
|
||||
|
||||
# --- Kernel release ---
|
||||
local kernel_str="unknown"
|
||||
[[ -r /proc/sys/kernel/osrelease ]] && read -r kernel_str < /proc/sys/kernel/osrelease
|
||||
|
||||
# --- Architecture (set by bash at startup from the ELF interpreter) ---
|
||||
local arch_str="${HOSTTYPE:-unknown}"
|
||||
|
||||
# --- Uptime (seconds from /proc/uptime) ---
|
||||
local uptime_str="unknown"
|
||||
if [[ -r /proc/uptime ]]; then
|
||||
local _upraw
|
||||
read -r _upraw _ < /proc/uptime
|
||||
local _upsec="${_upraw%%.*}"
|
||||
local _days=$(( _upsec / 86400 ))
|
||||
local _hours=$(( (_upsec % 86400) / 3600 ))
|
||||
local _mins=$(( (_upsec % 3600) / 60 ))
|
||||
local _secs=$(( _upsec % 60 ))
|
||||
uptime_str=""
|
||||
(( _days > 0 )) && uptime_str+="${_days}d "
|
||||
(( _hours > 0 || _days > 0 )) && uptime_str+="${_hours}h "
|
||||
uptime_str+="${_mins}m ${_secs}s"
|
||||
fi
|
||||
|
||||
# --- Load average ---
|
||||
local load_str="unknown"
|
||||
if [[ -r /proc/loadavg ]]; then
|
||||
local _l1 _l5 _l15
|
||||
read -r _l1 _l5 _l15 _ < /proc/loadavg
|
||||
load_str="${_l1} ${_l5} ${_l15} (1/5/15 min)"
|
||||
fi
|
||||
|
||||
# --- CPU model and logical/physical core count (pure bash) ---
|
||||
local cpu_model="unknown" cpu_threads=0
|
||||
local -A _seen_cores=()
|
||||
local _cur_phys="" _cpu_line
|
||||
if [[ -r /proc/cpuinfo ]]; then
|
||||
while IFS= read -r _cpu_line; do
|
||||
case "$_cpu_line" in
|
||||
"model name"*|"Model name"*|"Hardware"*)
|
||||
[[ "$cpu_model" == "unknown" ]] && cpu_model="${_cpu_line#*: }"
|
||||
;;
|
||||
"processor"*)
|
||||
cpu_threads=$(( cpu_threads + 1 ))
|
||||
;;
|
||||
"physical id"*)
|
||||
_cur_phys="${_cpu_line#*: }"
|
||||
;;
|
||||
"core id"*)
|
||||
_seen_cores["${_cur_phys}:${_cpu_line#*: }"]=1
|
||||
;;
|
||||
esac
|
||||
done < /proc/cpuinfo
|
||||
fi
|
||||
local cpu_cores="${#_seen_cores[@]}"
|
||||
(( cpu_cores == 0 )) && cpu_cores=$cpu_threads
|
||||
|
||||
# --- Memory (pure bash, /proc/meminfo, values in kB) ---
|
||||
# MemAvailable only exists since Linux 3.14; on older kernels it is
|
||||
# estimated as free + buffers + cached.
|
||||
local mem_total=0 mem_available=-1 mem_free=0 mem_buffers=0 mem_cached=0
|
||||
local swap_total=0 swap_free=0
|
||||
if [[ -r /proc/meminfo ]]; then
|
||||
local _mkey _mval _munit
|
||||
while read -r _mkey _mval _munit; do
|
||||
case "${_mkey%:}" in
|
||||
MemTotal) mem_total="$_mval" ;;
|
||||
MemAvailable) mem_available="$_mval" ;;
|
||||
MemFree) mem_free="$_mval" ;;
|
||||
Buffers) mem_buffers="$_mval" ;;
|
||||
Cached) mem_cached="$_mval" ;;
|
||||
SwapTotal) swap_total="$_mval" ;;
|
||||
SwapFree) swap_free="$_mval" ;;
|
||||
esac
|
||||
done < /proc/meminfo
|
||||
fi
|
||||
(( mem_available < 0 )) && mem_available=$(( mem_free + mem_buffers + mem_cached ))
|
||||
local mem_total_mib=$(( mem_total / 1024 ))
|
||||
local mem_used_mib=$(( (mem_total - mem_available) / 1024 ))
|
||||
local swap_total_mib=$(( swap_total / 1024 ))
|
||||
local swap_used_mib=$(( (swap_total - swap_free) / 1024 ))
|
||||
|
||||
# --- Process count (glob over numeric /proc entries) ---
|
||||
local proc_count=0 _pdir
|
||||
for _pdir in /proc/[0-9]*/; do
|
||||
[[ -d "$_pdir" ]] && proc_count=$(( proc_count + 1 ))
|
||||
done
|
||||
|
||||
# --- Shell and terminal ---
|
||||
local shell_str="${BASH:-bash} ${BASH_VERSION%\(*}"
|
||||
local term_str="${TERM:-unknown}"
|
||||
|
||||
# --- GPU (no external tools required; sysfs first, then /proc/bus/pci) ---
|
||||
local -a gpu_list=()
|
||||
local _gdir _gname _uev_key _uev_val _uev_driver _uev_pci
|
||||
# Preferred: sysfs drm — each card has a device/uevent file readable
|
||||
# without root, giving the PCI id and the kernel driver in use. A label
|
||||
# file (rarely provided by drivers) wins when present.
|
||||
for _gdir in /sys/class/drm/card[0-9]*/device; do
|
||||
[[ -d "$_gdir" ]] || continue
|
||||
# Connector entries (card0-eDP-1, card0-HDMI-A-1, ...) also carry a
|
||||
# "device" link to the same GPU: keep the plain "cardN" entries only.
|
||||
_gname="${_gdir%/device}"
|
||||
[[ "${_gname##*/}" =~ ^card[0-9]+$ ]] || continue
|
||||
_gname=""
|
||||
_uev_driver="" _uev_pci=""
|
||||
if [[ -r "$_gdir/uevent" ]]; then
|
||||
while IFS='=' read -r _uev_key _uev_val; do
|
||||
case "$_uev_key" in
|
||||
DRIVER) _uev_driver="$_uev_val" ;;
|
||||
PCI_ID) _uev_pci="$_uev_val" ;;
|
||||
esac
|
||||
done < "$_gdir/uevent"
|
||||
[[ -n "$_uev_pci" ]] && _gname="PCI ${_uev_pci}"
|
||||
[[ -n "$_uev_driver" ]] && _gname+=" (${_uev_driver})"
|
||||
fi
|
||||
if [[ -r "$_gdir/label" ]]; then
|
||||
read -r _gname < "$_gdir/label"
|
||||
elif [[ -r "$_gdir/../label" ]]; then
|
||||
read -r _gname < "$_gdir/../label"
|
||||
fi
|
||||
[[ -z "$_gname" ]] && _gname="unknown GPU"
|
||||
gpu_list+=("$_gname")
|
||||
done
|
||||
|
||||
# Fallback: /proc/bus/pci/devices. It holds no human-readable name: the
|
||||
# tab-separated fields are the bus address, vendor+device ids (8 hex
|
||||
# digits), IRQ, the base addresses and sizes, then the kernel driver name
|
||||
# as the last field when one is bound. Only well-known GPU drivers are
|
||||
# reported, as vendor ids alone also match every other device of a vendor.
|
||||
if (( ${#gpu_list[@]} == 0 )) && [[ -r /proc/bus/pci/devices ]]; then
|
||||
local -a _pci=()
|
||||
local _pci_id _pci_drv
|
||||
while IFS=$'\t' read -r -a _pci; do
|
||||
_pci_drv="${_pci[-1]// /}"
|
||||
case "$_pci_drv" in
|
||||
i915|xe|amdgpu|radeon|nouveau|nvidia|virtio-pci|vmwgfx|qxl|bochs-drm|cirrus|mgag200|ast)
|
||||
_pci_id="${_pci[1]}"
|
||||
gpu_list+=("PCI ${_pci_id:0:4}:${_pci_id:4:4} (${_pci_drv})")
|
||||
;;
|
||||
esac
|
||||
done < /proc/bus/pci/devices
|
||||
fi
|
||||
|
||||
# --- Render ---
|
||||
# Colours are dropped under NO_COLOR, like everywhere else in the profile.
|
||||
local _lbl="${BIWhite:-}" _val="${ICyan:-}" _rst="${DEFAULTCOL:-}"
|
||||
if [[ -n ${NO_COLOR:-} ]]; then
|
||||
_lbl="" _val="" _rst=""
|
||||
fi
|
||||
|
||||
printf "\n"
|
||||
_row "Hostname" "$hostname_str"
|
||||
_row "OS" "${os_name}${os_version:+ $os_version}"
|
||||
_row "Kernel" "$kernel_str"
|
||||
_row "Arch" "$arch_str"
|
||||
_row "Uptime" "$uptime_str"
|
||||
_row "Load avg" "$load_str"
|
||||
_row "CPU" "$cpu_model"
|
||||
_row "Cores" "${cpu_cores} physical, ${cpu_threads} logical"
|
||||
if (( ${#gpu_list[@]} > 0 )); then
|
||||
local _gi
|
||||
for _gi in "${!gpu_list[@]}"; do
|
||||
local _gpu_lbl="GPU"
|
||||
(( ${#gpu_list[@]} > 1 )) && _gpu_lbl="GPU $(( _gi + 1 ))"
|
||||
_row "$_gpu_lbl" "${gpu_list[$_gi]}"
|
||||
done
|
||||
fi
|
||||
_row "Memory" "${mem_used_mib} MiB used / ${mem_total_mib} MiB total"
|
||||
if (( swap_total > 0 )); then
|
||||
_row "Swap" "${swap_used_mib} MiB used / ${swap_total_mib} MiB total"
|
||||
fi
|
||||
_row "Processes" "$proc_count"
|
||||
_row "Shell" "$shell_str"
|
||||
_row "Terminal" "$term_str"
|
||||
printf "\n"
|
||||
unset -f _row
|
||||
}
|
||||
export -f pinfo
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# Display system general information
|
||||
# ------------------------------------------------------------------------------
|
||||
# Usage: showinfo
|
||||
showinfo()
|
||||
{
|
||||
echo -e "\n"
|
||||
if command -v figlet >/dev/null 2>&1; then
|
||||
if [[ -s /usr/share/figlet/ansi_shadow.flf ]]; then
|
||||
local figopt="-f ansi_shadow"
|
||||
fi
|
||||
figlet -k $(hostname) $figopt
|
||||
else
|
||||
echo "$(hostname -f)"
|
||||
local PARSED
|
||||
|
||||
PARSED=$(getopt -o h --long help -n 'showinfo' -- "$@")
|
||||
# shellcheck disable=SC2181 # getopt return code is checked immediately after
|
||||
if [[ $? -ne 0 ]]; then
|
||||
disp E "Invalid options, use \"showinfo --help\" to display usage."
|
||||
return 1
|
||||
fi
|
||||
echo ""
|
||||
if command -v neofetch >/dev/null 2>&1; then
|
||||
neofetch
|
||||
# Restore the getopt-normalised (shell-quoted) argument list
|
||||
local -a _pargs=()
|
||||
_getopt_split _pargs "$PARSED"
|
||||
set -- "${_pargs[@]}"
|
||||
|
||||
while true; do
|
||||
case "$1" in
|
||||
-h|--help)
|
||||
printf "showinfo: Display system information (hostname, kernel, uptime and fetch output when available).\n"
|
||||
printf "Usage: showinfo\n"
|
||||
return 0
|
||||
;;
|
||||
--)
|
||||
shift
|
||||
break
|
||||
;;
|
||||
*)
|
||||
disp E "Invalid options, use \"showinfo --help\" to display usage."
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
local hostname_str figopt=()
|
||||
# /proc first: the hostname binary is missing from minimal installations
|
||||
if [[ -r /proc/sys/kernel/hostname ]]; then
|
||||
read -r hostname_str < /proc/sys/kernel/hostname
|
||||
else
|
||||
(
|
||||
if [[ -s /etc/os-release ]]; then
|
||||
. /etc/os-release
|
||||
echo "$NAME $VERSION"
|
||||
else
|
||||
cat /proc/version
|
||||
fi
|
||||
echo "Uptime: $(uptime)"
|
||||
)
|
||||
hostname_str="${HOSTNAME:-$(hostname 2>/dev/null)}"
|
||||
fi
|
||||
|
||||
printf "\n"
|
||||
if command -v figlet >/dev/null 2>&1; then
|
||||
[[ -s /usr/share/figlet/ansi_shadow.flf ]] && \
|
||||
figopt=(-f ansi_shadow)
|
||||
figlet -k "${figopt[@]}" "$hostname_str"
|
||||
else
|
||||
printf "%s\n" "$hostname_str"
|
||||
fi
|
||||
|
||||
printf "\n"
|
||||
if command -v fastfetch >/dev/null 2>&1; then
|
||||
fastfetch
|
||||
elif command -v neofetch >/dev/null 2>&1; then
|
||||
neofetch
|
||||
else
|
||||
pinfo "$@"
|
||||
fi
|
||||
}
|
||||
export -f showinfo
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
|
||||
# EOF
|
||||
|
||||
+267
-22
@@ -1,35 +1,280 @@
|
||||
#!/usr/bin/env bash
|
||||
# ------------------------------------------------------------------------------
|
||||
# Change locale to French
|
||||
# Copyright (c) 2013-2026 Geoffray Levasseur <fatalerrors@geoffray-levasseur.org>
|
||||
# Protected by the BSD3 license. Please read bellow for details.
|
||||
#
|
||||
# * Redistribution and use in source and binary forms,
|
||||
# * with or without modification, are permitted provided
|
||||
# * that the following conditions are met:
|
||||
# *
|
||||
# * Redistributions of source code must retain the above
|
||||
# * copyright notice, this list of conditions and the
|
||||
# * following disclaimer.
|
||||
# *
|
||||
# * Redistributions in binary form must reproduce the above
|
||||
# * copyright notice, this list of conditions and the following
|
||||
# * disclaimer in the documentation and/or other materials
|
||||
# * provided with the distribution.
|
||||
# *
|
||||
# * Neither the name of the copyright holder nor the names
|
||||
# * of any other contributors may be used to endorse or
|
||||
# * promote products derived from this software without
|
||||
# * specific prior written permission.
|
||||
# *
|
||||
# * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
|
||||
# * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
|
||||
# * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
|
||||
# * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
# * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
|
||||
# * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
# * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
|
||||
# * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
# * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||
# * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
|
||||
# * OF SUCH DAMAGE.
|
||||
# ------------------------------------------------------------------------------
|
||||
setfr ()
|
||||
{
|
||||
# Set fr locale definitions
|
||||
export LANG=fr_FR.UTF-8
|
||||
export LC_MESSAGES=fr_FR.UTF-8
|
||||
export LC_ALL=fr_FR.UTF-8
|
||||
}
|
||||
export -f setfr
|
||||
|
||||
# Common helpers (helpers/common.sh)
|
||||
# shellcheck source=../helpers/common.sh
|
||||
declare -F _profile_load_helpers >/dev/null || . "$(dirname -- "${BASH_SOURCE[0]}")/../helpers/common.sh"
|
||||
|
||||
load_conf lang
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# Change locale to C standard
|
||||
# ------------------------------------------------------------------------------
|
||||
setc ()
|
||||
# check if a given locale is installed on the system.
|
||||
# The comparison is tolerant to the various ways the codeset can be written
|
||||
# across distributions (UTF-8, UTF8, utf8, utf-8, ISO-8859-1, iso88591, ...).
|
||||
# On success the canonical name, exactly as the system spells it, is printed on
|
||||
# stdout so callers can feed it verbatim to `export LANG=...` etc.
|
||||
# Usage: canonical=$(locale_check <locale>) || handle-missing
|
||||
locale_check()
|
||||
{
|
||||
if [[ "${1:-}" == -h || "${1:-}" == --help ]]; then
|
||||
printf "locale_check: tell whether a locale is installed.\n"
|
||||
printf "Usage: locale_check <locale>\n"
|
||||
printf "Prints the name of the locale as the system spells it; status 1 if it is not installed.\n"
|
||||
return 0
|
||||
fi
|
||||
local requested="${1:-}"
|
||||
[[ -z $requested ]] && { disp W "No locale given to check."; return 1; }
|
||||
|
||||
# Split "lang_TERRITORY[.codeset][@modifier]" into base, codeset, modifier.
|
||||
local req_body="$requested" req_cs="" req_mod=""
|
||||
[[ $req_body == *@* ]] && { req_mod="${req_body##*@}"; req_body="${req_body%@*}"; }
|
||||
[[ $req_body == *.* ]] && { req_cs="${req_body##*.}"; req_body="${req_body%.*}"; }
|
||||
|
||||
# Normalise the codeset: lowercase then strip every non-alphanumeric char so
|
||||
# that UTF-8, UTF8, utf8 and utf-8 all compare equal (likewise ISO variants).
|
||||
local req_cs_norm="${req_cs,,}"
|
||||
req_cs_norm="${req_cs_norm//[^a-z0-9]/}"
|
||||
|
||||
local cand body cs mod cs_norm fallback=""
|
||||
while IFS= read -r cand; do
|
||||
[[ -z $cand ]] && continue
|
||||
body="$cand"; cs=""; mod=""
|
||||
[[ $body == *@* ]] && { mod="${body##*@}"; body="${body%@*}"; }
|
||||
[[ $body == *.* ]] && { cs="${body##*.}"; body="${body%.*}"; }
|
||||
cs_norm="${cs,,}"
|
||||
cs_norm="${cs_norm//[^a-z0-9]/}"
|
||||
|
||||
# Base language and modifier must match exactly.
|
||||
[[ $body == "$req_body" && $mod == "$req_mod" ]] || continue
|
||||
# A requested codeset must match (normalised); if none was requested,
|
||||
# accept any codeset the system provides.
|
||||
[[ -z $req_cs_norm || $cs_norm == "$req_cs_norm" ]] || continue
|
||||
|
||||
# With no codeset requested, UTF-8 is preferred: "locale -a" may list
|
||||
# an older charset first (fr_FR, fr_FR.iso88591, fr_FR.utf8). Any other
|
||||
# match is kept as a fallback in case no UTF-8 variant exists.
|
||||
if [[ -z $req_cs_norm && $cs_norm != "utf8" ]]; then
|
||||
[[ -z $fallback ]] && fallback="$cand"
|
||||
continue
|
||||
fi
|
||||
|
||||
printf '%s\n' "$cand"
|
||||
return 0
|
||||
done < <(locale -a 2>/dev/null)
|
||||
|
||||
if [[ -n $fallback ]]; then
|
||||
printf '%s\n' "$fallback"
|
||||
return 0
|
||||
fi
|
||||
|
||||
disp W "Locale '$requested' is not installed on this system."
|
||||
return 1
|
||||
}
|
||||
export -f locale_check
|
||||
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# Change locale to the given one in parameter
|
||||
# Usage: setlocale <locale>
|
||||
setlocale()
|
||||
{
|
||||
local PARSED
|
||||
PARSED=$(getopt -o h --long help -n 'setlocale' -- "$@")
|
||||
# shellcheck disable=SC2181 # getopt return code is checked immediately after
|
||||
if [[ $? -ne 0 ]]; then
|
||||
disp E "Invalid options, use \"setlocale --help\" to display usage."
|
||||
return 1
|
||||
fi
|
||||
# Restore the getopt-normalised (shell-quoted) argument list
|
||||
local -a _pargs=()
|
||||
_getopt_split _pargs "$PARSED"
|
||||
set -- "${_pargs[@]}"
|
||||
while true; do
|
||||
case "$1" in
|
||||
-h|--help)
|
||||
printf "setlocale: Configure system environment locale variables.\n\n"
|
||||
printf "Usage: setlocale <locale>\n\n"
|
||||
printf "Options:\n"
|
||||
printf " -h, --help Display this help screen\n"
|
||||
return 0
|
||||
;;
|
||||
--)
|
||||
shift
|
||||
break
|
||||
;;
|
||||
*)
|
||||
disp E "Invalid options, use \"setlocale --help\" to display usage."
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
local loc=$1
|
||||
[[ -z $loc ]] && disp E "No locale specified." && return 1
|
||||
|
||||
# Resolve to the exact spelling installed on this system, tolerant to the
|
||||
# various codeset writings (UTF-8, UTF8, utf8, utf-8, ...).
|
||||
loc=$(locale_check "$loc") || return 1
|
||||
|
||||
# LC_ALL overrides every other variable (setc sets it), and so does any
|
||||
# per-category variable inherited from the environment: drop them all so
|
||||
# that the new locale really takes effect.
|
||||
unset LC_ALL LC_ADDRESS LC_IDENTIFICATION LC_MEASUREMENT LC_NAME \
|
||||
LC_PAPER LC_TELEPHONE
|
||||
|
||||
export LANG=$loc
|
||||
export LC_MESSAGES=$loc
|
||||
export LC_TIME=$loc
|
||||
export LC_NUMERIC=$loc
|
||||
export LC_MONETARY=$loc
|
||||
export LC_COLLATE=$loc
|
||||
export LC_CTYPE=$loc
|
||||
|
||||
disp I "Locale set to $loc."
|
||||
}
|
||||
export -f setlocale
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# Special case : change locale to C standard
|
||||
# Usage: setc
|
||||
setc()
|
||||
{
|
||||
if [[ "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then
|
||||
printf "setc: Set the console locale to the standard C (POSIX) locale.\n"
|
||||
printf "Usage: setc\n"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Locale definitions
|
||||
export LANG=C
|
||||
export LC_MESSAGES=C
|
||||
export LC_ALL=C
|
||||
export LANG=C
|
||||
disp I "Locale changed to standard C (POSIX)."
|
||||
}
|
||||
export -f setc
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# Change locale to US (needed by Steam)
|
||||
# ------------------------------------------------------------------------------
|
||||
setus ()
|
||||
# Build dynamic locale shortcuts from SET_LOCALE
|
||||
# Expected format:
|
||||
# SET_LOCALE="fr:fr_FR.UTF-8,us:en_US.UTF-8,es:es_ES.UTF-8"
|
||||
# This creates functions:
|
||||
# setfr, setus, setes, ...
|
||||
build_locale_shortcuts()
|
||||
{
|
||||
# Locale definitions
|
||||
export LANG=en_US.UTF-8
|
||||
export LC_MESSAGES=en_US.UTF-8
|
||||
export LC_ALL=en_US.UTF-8
|
||||
local cfg="${SET_LOCALE:-}"
|
||||
local item="" alias="" loc="" fname=""
|
||||
local -a locale_items=()
|
||||
|
||||
[[ -z "$cfg" ]] && return 0
|
||||
|
||||
IFS=',' read -r -a locale_items <<< "$cfg"
|
||||
for item in "${locale_items[@]}"; do
|
||||
# Trim surrounding spaces
|
||||
item="${item#"${item%%[![:space:]]*}"}"
|
||||
item="${item%"${item##*[![:space:]]}"}"
|
||||
|
||||
[[ -z "$item" ]] && continue
|
||||
|
||||
if [[ "$item" != *:* ]]; then
|
||||
disp W "Ignoring invalid SET_LOCALE entry: '$item' (expected alias:locale)."
|
||||
continue
|
||||
fi
|
||||
|
||||
alias="${item%%:*}"
|
||||
loc="${item#*:}"
|
||||
|
||||
# Trim alias/locale spaces
|
||||
alias="${alias#"${alias%%[![:space:]]*}"}"
|
||||
alias="${alias%"${alias##*[![:space:]]}"}"
|
||||
loc="${loc#"${loc%%[![:space:]]*}"}"
|
||||
loc="${loc%"${loc##*[![:space:]]}"}"
|
||||
|
||||
# Validate alias for safe function names
|
||||
if [[ ! "$alias" =~ ^[A-Za-z_][A-Za-z0-9_]*$ ]]; then
|
||||
disp W "Ignoring unsafe locale alias '$alias' in SET_LOCALE."
|
||||
continue
|
||||
fi
|
||||
|
||||
[[ -z "$loc" ]] && {
|
||||
disp W "Ignoring empty locale for alias '$alias' in SET_LOCALE."
|
||||
continue
|
||||
}
|
||||
|
||||
fname="set${alias}"
|
||||
|
||||
# The generated function must never replace the real commands
|
||||
if [[ "$fname" == "setlocale" || "$fname" == "setc" ]]; then
|
||||
disp W "Ignoring locale alias '$alias' in SET_LOCALE: '$fname' is a profile command."
|
||||
continue
|
||||
fi
|
||||
|
||||
# Collision warning. A function that already calls setlocale is one of
|
||||
# these shortcuts, typically inherited through the environment from a
|
||||
# parent shell that loaded the profile: replacing it is not worth a
|
||||
# warning at every nested shell.
|
||||
if declare -F "$fname" >/dev/null 2>&1 && \
|
||||
! declare -f "$fname" 2>/dev/null | grep -q 'setlocale'; then
|
||||
disp W "Overriding existing function '$fname'."
|
||||
fi
|
||||
|
||||
# Build function dynamically. %q quotes the locale so that nothing in
|
||||
# the configuration value can be interpreted by eval. The shortcut
|
||||
# answers --help without changing anything ("help <command>" calls it).
|
||||
local qloc
|
||||
printf -v qloc '%q' "$loc"
|
||||
eval "${fname}() {
|
||||
if [[ \"\${1:-}\" == -h || \"\${1:-}\" == --help ]]; then
|
||||
printf '%s: switch the console locale (see setlocale --help).\n' \"\${FUNCNAME[0]}\"
|
||||
return 0
|
||||
fi
|
||||
setlocale ${qloc}
|
||||
}"
|
||||
# shellcheck disable=SC2163
|
||||
export -f "$fname"
|
||||
done
|
||||
|
||||
unset cfg item alias loc fname locale_items qloc
|
||||
}
|
||||
export -f setus
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
# Dynamically build locale functions based on SET_LOCALE configuration
|
||||
build_locale_shortcuts
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# EOF
|
||||
|
||||
+508
-31
@@ -1,49 +1,526 @@
|
||||
#!/usr/bin/env bash
|
||||
# ------------------------------------------------------------------------------
|
||||
# Copyright (c) 2013-2026 Geoffray Levasseur <fatalerrors@geoffray-levasseur.org>
|
||||
# Protected by the BSD3 license. Please read bellow for details.
|
||||
#
|
||||
# * Redistribution and use in source and binary forms,
|
||||
# * with or without modification, are permitted provided
|
||||
# * that the following conditions are met:
|
||||
# *
|
||||
# * Redistributions of source code must retain the above
|
||||
# * copyright notice, this list of conditions and the
|
||||
# * following disclaimer.
|
||||
# *
|
||||
# * Redistributions in binary form must reproduce the above
|
||||
# * copyright notice, this list of conditions and the following
|
||||
# * disclaimer in the documentation and/or other materials
|
||||
# * provided with the distribution.
|
||||
# *
|
||||
# * Neither the name of the copyright holder nor the names
|
||||
# * of any other contributors may be used to endorse or
|
||||
# * promote products derived from this software without
|
||||
# * specific prior written permission.
|
||||
# *
|
||||
# * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
|
||||
# * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
|
||||
# * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
|
||||
# * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
# * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
|
||||
# * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
# * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
|
||||
# * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
# * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||
# * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
|
||||
# * OF SUCH DAMAGE.
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
# shellcheck disable=SC2317,SC2329 # nested helper functions are called through their name (shellcheck cannot follow them)
|
||||
|
||||
load_conf "net"
|
||||
|
||||
# Common helpers, then the ones private to this module (helpers/net.sh)
|
||||
# shellcheck source=../helpers/common.sh
|
||||
declare -F _profile_load_helpers >/dev/null || . "$(dirname -- "${BASH_SOURCE[0]}")/../helpers/common.sh"
|
||||
_profile_load_helpers "${BASH_SOURCE[0]}"
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# Download a resource using curl, wget, or fetch.
|
||||
# Usage: dwl [-t <seconds>] [-r|--resume] <url> [output_file]
|
||||
dwl()
|
||||
{
|
||||
local timeout=""
|
||||
local resume=0
|
||||
|
||||
local _res="${DWL_DEFAULT_RESUME:-}"
|
||||
case "${_res,,}" in
|
||||
1|true|yes|on)
|
||||
resume=1
|
||||
;;
|
||||
esac
|
||||
|
||||
# Parse leading options before the URL.
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--help|-h)
|
||||
echo "Usage: dwl [-t <seconds>|--timeout <seconds>] [-r|--resume] [--no-resume] <url> [output_file]"
|
||||
echo "Downloads a resource using curl, wget, or fetch."
|
||||
echo ""
|
||||
echo "Arguments:"
|
||||
echo " -t, --timeout Maximum time in seconds to wait for the transfer."
|
||||
echo " -r, --resume Resume an interrupted download when possible (file output only)."
|
||||
echo " --no-resume Disable resume mode even if enabled in config."
|
||||
echo " url The full URL to download (http/https/ftp)."
|
||||
echo " output_file (Optional) Path to save the file. If omitted, prints to stdout."
|
||||
return 0
|
||||
;;
|
||||
-t|--timeout)
|
||||
[[ -z "${2:-}" || ! "${2:-}" =~ ^[0-9]+$ ]] && {
|
||||
echo "Error: --timeout requires a positive integer argument." >&2
|
||||
return 1
|
||||
}
|
||||
timeout="$2"
|
||||
shift 2
|
||||
;;
|
||||
-r|--resume)
|
||||
resume=1
|
||||
shift
|
||||
;;
|
||||
--no-resume)
|
||||
resume=0
|
||||
shift
|
||||
;;
|
||||
--)
|
||||
shift
|
||||
break
|
||||
;;
|
||||
-*)
|
||||
echo "Error: Unknown option '$1'. Try 'dwl --help'." >&2
|
||||
return 1
|
||||
;;
|
||||
*)
|
||||
break
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
case "${1:-}" in
|
||||
"")
|
||||
echo "Error: URL argument is missing." >&2
|
||||
echo "Try 'dwl --help' for usage." >&2
|
||||
return 1
|
||||
;;
|
||||
http://*|https://*|ftp://*) ;;
|
||||
*)
|
||||
echo "Error: '$1' does not look like a valid URL. Must start with http://, https://, or ftp://" >&2
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
|
||||
local url="$1"
|
||||
local output="${2:-}"
|
||||
|
||||
# Honour preferred tool from configuration; fall back to auto-detection.
|
||||
local preferred="${DWL_PREFERRED_TOOL:-}"
|
||||
|
||||
_try_curl()
|
||||
{
|
||||
# -f: an HTTP error (404, 500, ...) is a failure, not a saved error
|
||||
# page; -S keeps error messages visible despite -s.
|
||||
local args=(-fsSL)
|
||||
[[ -n "$timeout" ]] && args+=(--max-time "$timeout" --connect-timeout "$timeout")
|
||||
if [[ -z "$output" ]]; then
|
||||
if (( resume == 1 )); then
|
||||
echo "Warning: --resume requires an output file; ignoring resume mode for stdout." >&2
|
||||
fi
|
||||
curl "${args[@]}" "$url"
|
||||
else
|
||||
(( resume == 1 )) && args+=(-C -)
|
||||
curl "${args[@]}" -o "$output" "$url"
|
||||
fi
|
||||
}
|
||||
|
||||
_try_wget()
|
||||
{
|
||||
local args=(-q)
|
||||
[[ -n "$timeout" ]] && args+=(--timeout="$timeout")
|
||||
(( resume == 1 )) && args+=(-c)
|
||||
if [[ -z "$output" ]]; then
|
||||
wget "${args[@]}" -O- "$url"
|
||||
else
|
||||
wget "${args[@]}" -O "$output" "$url"
|
||||
fi
|
||||
}
|
||||
|
||||
_try_fetch()
|
||||
{
|
||||
local args=()
|
||||
[[ -n "$timeout" ]] && args+=(-T "$timeout")
|
||||
if (( resume == 1 )); then
|
||||
echo "Warning: resume mode is not supported with fetch; continuing without resume." >&2
|
||||
fi
|
||||
if [[ -z "$output" ]]; then
|
||||
fetch "${args[@]}" -o - "$url"
|
||||
else
|
||||
fetch "${args[@]}" -o "$output" "$url"
|
||||
fi
|
||||
}
|
||||
|
||||
# Pick the tool: the configured one, otherwise the first available.
|
||||
# Helpers are removed on every exit path; the exit status of the transfer
|
||||
# itself is returned (the final "unset -f" must not mask it).
|
||||
local tool=""
|
||||
if [[ -n "$preferred" ]]; then
|
||||
if ! command -v "$preferred" >/dev/null 2>&1; then
|
||||
echo "Error: preferred download tool '$preferred' is not installed." >&2
|
||||
unset -f _try_curl _try_wget _try_fetch
|
||||
return 1
|
||||
fi
|
||||
case "$preferred" in
|
||||
curl|wget|fetch) tool="$preferred" ;;
|
||||
*)
|
||||
echo "Error: DWL_PREFERRED_TOOL '$preferred' is not supported (use curl, wget or fetch)." >&2
|
||||
unset -f _try_curl _try_wget _try_fetch
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
elif command -v curl >/dev/null 2>&1; then
|
||||
tool=curl
|
||||
elif command -v wget >/dev/null 2>&1; then
|
||||
tool=wget
|
||||
elif command -v fetch >/dev/null 2>&1; then
|
||||
tool=fetch
|
||||
else
|
||||
echo "Error: No download utility (curl, wget, or fetch) found." >&2
|
||||
unset -f _try_curl _try_wget _try_fetch
|
||||
return 1
|
||||
fi
|
||||
|
||||
local rc=0
|
||||
"_try_${tool}" || rc=$?
|
||||
unset -f _try_curl _try_wget _try_fetch
|
||||
return "$rc"
|
||||
}
|
||||
export -f dwl
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# Determine if parameter is a valid IPv4 address
|
||||
# ------------------------------------------------------------------------------
|
||||
isipv4 ()
|
||||
# Usage: isipv4 <ip_address>
|
||||
isipv4()
|
||||
{
|
||||
# Set up local variables
|
||||
local ip=$1
|
||||
local ip=$1
|
||||
[[ -z $ip ]] && return 1
|
||||
|
||||
# Start with a regex format test
|
||||
if [[ $ip =~ ^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$ ]]; then
|
||||
local old_ifs=$IFS
|
||||
IFS="."
|
||||
ip=($ip)
|
||||
IFS=$old_ifs
|
||||
if [[ ${ip[0]} -le 255 && ${ip[1]} -le 255 \
|
||||
&& ${ip[2]} -le 255 && ${ip[3]} -le 255 ]]; then
|
||||
if [[ -t 1 ]]; then
|
||||
echo "The given IPv4 is valid."
|
||||
fi
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
if [[ -t 1 ]]; then
|
||||
echo "The given parameter is NOT a valid IPv4."
|
||||
# Start with a regex format test (four octets)
|
||||
if [[ $ip =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]]; then
|
||||
local -a ip_arr=()
|
||||
IFS='.' read -r -a ip_arr <<< "$ip"
|
||||
|
||||
# Ensure each octet is between 0 and 255
|
||||
local oct
|
||||
for oct in "${ip_arr[@]}"; do
|
||||
# Reject leading plus/minus or empty entries
|
||||
if [[ -z $oct || $oct =~ [^0-9] ]]; then
|
||||
[[ -t 1 ]] && disp E "The given parameter is NOT a valid IPv4."
|
||||
return 1
|
||||
fi
|
||||
# 10# forces base-10 so leading-zero octets (08, 09) are not read as octal
|
||||
if (( 10#$oct > 255 )); then
|
||||
[[ -t 1 ]] && disp E "The given parameter is NOT a valid IPv4."
|
||||
return 1
|
||||
fi
|
||||
done
|
||||
|
||||
[[ -t 1 ]] && disp I "The given IPv4 is valid."
|
||||
return 0
|
||||
fi
|
||||
|
||||
[[ -t 1 ]] && disp E "The given parameter is NOT a valid IPv4."
|
||||
return 1
|
||||
}
|
||||
export -f isipv4
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# Determine if parameter is a valid IPv4 address
|
||||
# ------------------------------------------------------------------------------
|
||||
isipv6 ()
|
||||
# Determine if parameter is a valid IPv6 address
|
||||
# Usage: isipv6 <ip_address>
|
||||
# Accepts full and "::"-compressed forms, an embedded IPv4 suffix
|
||||
# (e.g. ::ffff:192.0.2.1) and an optional RFC 4007 zone id (e.g. fe80::1%eth0).
|
||||
isipv6()
|
||||
{
|
||||
local ip="$1"
|
||||
local regex='^([0-9a-fA-F]{0,4}:){1,7}[0-9a-fA-F]{0,4}$'
|
||||
if [[ $ip =~ $regex ]]; then
|
||||
if [[ -t 1 ]]; then
|
||||
echo "The given IPv6 is valid."
|
||||
fi
|
||||
return 0
|
||||
fi
|
||||
if [[ -t 1 ]]; then
|
||||
echo "The given parameter is not a valid IPv6."
|
||||
local valid=0
|
||||
|
||||
# A single-pass validation; any failing check breaks out with valid=0.
|
||||
while true; do
|
||||
[[ -z $ip ]] && break
|
||||
|
||||
# Strip an optional zone identifier (fe80::1%eth0); reject empty/multiple.
|
||||
if [[ $ip == *%* ]]; then
|
||||
[[ $ip == *%*%* ]] && break
|
||||
[[ -z ${ip#*%} ]] && break
|
||||
ip="${ip%%[%]*}"
|
||||
fi
|
||||
|
||||
# Reject ':::' and more than one '::' compression.
|
||||
[[ $ip == *:::* ]] && break
|
||||
local compressed=0
|
||||
if [[ $ip == *::* ]]; then
|
||||
[[ ${ip#*::} == *::* ]] && break
|
||||
compressed=1
|
||||
fi
|
||||
|
||||
# A lone leading/trailing ':' that is not part of a '::' is invalid.
|
||||
[[ $ip == :* && $ip != ::* ]] && break
|
||||
[[ $ip == *: && $ip != *:: ]] && break
|
||||
|
||||
# An embedded IPv4 is allowed only as the trailing 32 bits. Validate it,
|
||||
# then replace it with two zero hextets so the rest is a clean hextet list.
|
||||
if [[ $ip == *.* ]]; then
|
||||
local v4="${ip##*:}"
|
||||
[[ $v4 == *.*.*.* ]] || break
|
||||
local -a oct=()
|
||||
IFS='.' read -ra oct <<< "$v4"
|
||||
[[ ${#oct[@]} -eq 4 ]] || break
|
||||
local o v4ok=1
|
||||
for o in "${oct[@]}"; do
|
||||
if [[ ! $o =~ ^[0-9]{1,3}$ ]] || (( 10#$o > 255 )); then
|
||||
v4ok=0
|
||||
break
|
||||
fi
|
||||
done
|
||||
(( v4ok )) || break
|
||||
ip="${ip%"$v4"}0:0"
|
||||
fi
|
||||
|
||||
# Count the hextets on each side of an optional '::'.
|
||||
local left right seg part total=0 ok=1
|
||||
if (( compressed )); then
|
||||
left="${ip%%::*}"
|
||||
right="${ip#*::}"
|
||||
else
|
||||
left="$ip"
|
||||
right=""
|
||||
fi
|
||||
|
||||
for seg in "$left" "$right"; do
|
||||
[[ -z $seg ]] && continue
|
||||
local -a parts=()
|
||||
IFS=':' read -ra parts <<< "$seg"
|
||||
for part in "${parts[@]}"; do
|
||||
[[ $part =~ ^[0-9a-fA-F]{1,4}$ ]] || { ok=0; break; }
|
||||
total=$(( total + 1 ))
|
||||
done
|
||||
(( ok )) || break
|
||||
done
|
||||
(( ok )) || break
|
||||
|
||||
# Without '::' there must be exactly 8 hextets; with it, fewer than 8
|
||||
# (the '::' expands to at least one zero hextet).
|
||||
if (( compressed )); then
|
||||
(( total <= 7 )) || break
|
||||
else
|
||||
(( total == 8 )) || break
|
||||
fi
|
||||
|
||||
valid=1
|
||||
break
|
||||
done
|
||||
|
||||
if (( valid )); then
|
||||
[[ -t 1 ]] && disp I "The given IPv6 is valid."
|
||||
return 0
|
||||
fi
|
||||
[[ -t 1 ]] && disp E "The given parameter is not a valid IPv6."
|
||||
return 1
|
||||
}
|
||||
export -f isipv6
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# Encode a string so it can be used in a URL path or query parameter: every
|
||||
# byte outside the unreserved set becomes %XX (UTF-8 bytes included, space is
|
||||
# %20, valid in both a path and a query).
|
||||
# Usage: urlencode <string>
|
||||
urlencode()
|
||||
{
|
||||
# LC_ALL (not LANG) is what overrides all the locale variables: with the
|
||||
# C locale each character is one byte, so multibyte characters are encoded
|
||||
# byte by byte (é -> %C3%A9) whatever the user's locale is.
|
||||
local LC_ALL=C
|
||||
local str="$*"
|
||||
local length="${#str}"
|
||||
local i
|
||||
for (( i = 0; i < length; i++ )); do
|
||||
local c="${str:i:1}"
|
||||
case "$c" in
|
||||
[a-zA-Z0-9.~_-]) printf '%s' "$c" ;;
|
||||
*) printf '%%%02X' "'$c" ;;
|
||||
esac
|
||||
done
|
||||
}
|
||||
export -f urlencode
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# Fetch and display external IP information
|
||||
# Usage: myextip [-i|--ip] [-s|--isp] [-l|--loc] [-c|--coord]
|
||||
# If no option is provided, all information will be displayed.
|
||||
# Options:
|
||||
# -h, --help Display help screen
|
||||
# -i, --ip Display only the external IP address
|
||||
# -s, --isp Display only the ISP name
|
||||
# -l, --loc Display only the location (city, region, country)
|
||||
# -c, --coord Display only the coordinates (latitude, longitude)
|
||||
# -a, --as Display only the Autonomous System (AS) information
|
||||
# -R, --raw Display raw JSON response
|
||||
myextip()
|
||||
{
|
||||
local show_ip=false show_isp=false show_loc=false
|
||||
local show_coord=false show_as=false show_raw=false
|
||||
local all=true
|
||||
|
||||
# Parse arguments
|
||||
while [[ "$#" -gt 0 ]]; do
|
||||
case "$1" in
|
||||
-i|--ip)
|
||||
show_ip=true
|
||||
all=false
|
||||
;;
|
||||
-s|--isp)
|
||||
show_isp=true
|
||||
all=false
|
||||
;;
|
||||
-l|--loc)
|
||||
show_loc=true
|
||||
all=false
|
||||
;;
|
||||
-c|--coord)
|
||||
show_coord=true
|
||||
all=false
|
||||
;;
|
||||
-a|--as)
|
||||
show_as=true
|
||||
all=false
|
||||
;;
|
||||
-R|--raw)
|
||||
all=false
|
||||
show_raw=true
|
||||
;;
|
||||
-h|--help)
|
||||
printf "Fetch and display external IP information.\n\n"
|
||||
printf "Usage: myextip [-i|--ip] [-s|--isp] [-l|--loc] [-c|--coord] [-a|--as] [-R|--raw]\n\n"
|
||||
printf "Options:\n"
|
||||
printf "\t-h, --help\tDisplay this help screen\n"
|
||||
printf "\t-i, --ip\tDisplay only the external IP address\n"
|
||||
printf "\t-s, --isp\tDisplay only the ISP name\n"
|
||||
printf "\t-l, --loc\tDisplay only the location (city, region, country)\n"
|
||||
printf "\t-c, --coord\tDisplay only the coordinates (latitude, longitude)\n"
|
||||
printf "\t-a, --as\tDisplay only the Autonomous System (AS) information\n"
|
||||
printf "\t-R, --raw\tDisplay raw JSON response\n"
|
||||
return 0
|
||||
;;
|
||||
--)
|
||||
shift
|
||||
break
|
||||
;;
|
||||
*)
|
||||
disp E "Unknown option: $1, use \"myextip --help\" to display usage."
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
shift
|
||||
done
|
||||
|
||||
# Fetch data. The endpoint can be changed with the MYEXTIP_DEFAULT_URL
|
||||
# configuration key; the default is the HTTPS ipinfo.io JSON API (no key
|
||||
# needed). ip-api.com is not usable: its free tier is HTTP only.
|
||||
local api_url="${MYEXTIP_DEFAULT_URL:-https://ipinfo.io/json}"
|
||||
if [[ $api_url != https://* ]]; then
|
||||
disp W "The lookup service ($api_url) is not HTTPS: the answer is not protected."
|
||||
fi
|
||||
|
||||
local response
|
||||
if ! response=$(dwl -t 10 "$api_url"); then
|
||||
disp E "Failed to fetch external IP information from $api_url"
|
||||
return 2
|
||||
fi
|
||||
|
||||
if [[ "$show_raw" == true ]]; then
|
||||
printf '%s\n' "$response"
|
||||
return 0
|
||||
fi
|
||||
|
||||
if ! command -v jq >/dev/null 2>&1; then
|
||||
disp W "jq is not installed, displaying raw JSON response."
|
||||
printf '%s\n' "$response"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# The first jq stage turns the answer of any supported provider into one
|
||||
# common object, so the display stage below does not care where the data
|
||||
# comes from:
|
||||
# ipinfo.io : ip, org "AS<n> <name>", city, region, country (code),
|
||||
# loc "lat,lon", timezone (string), postal
|
||||
# ipwho.is : ip, connection{asn,org,isp}, country (name), latitude,
|
||||
# longitude, timezone{id}, postal (recognised by .connection)
|
||||
# ip-api.com: query, isp, as, regionName, lat, lon, timezone, zip
|
||||
# (recognised by .query)
|
||||
# A missing field is simply left out of the output. An answer without any
|
||||
# IP (error message, rate limit, ...) is reported as unexpected.
|
||||
local out
|
||||
out=$(printf '%s' "$response" | jq -r \
|
||||
--argjson all "$all" --argjson ip "$show_ip" \
|
||||
--argjson isp "$show_isp" --argjson loc "$show_loc" \
|
||||
--argjson coord "$show_coord" --argjson as "$show_as" '
|
||||
def clean: map(select(. != null and . != "")) ;
|
||||
(if (.connection | type) == "object" then
|
||||
{ ip: .ip,
|
||||
isp: (.connection.isp // .connection.org),
|
||||
as: (if .connection.asn then
|
||||
(["AS\(.connection.asn)", .connection.org] | clean | join(" "))
|
||||
else null end),
|
||||
city: .city, region: .region, country: .country, zip: .postal,
|
||||
lat: .latitude, lon: .longitude, tz: .timezone.id }
|
||||
elif .query then
|
||||
{ ip: .query, isp: .isp, as: .as, city: .city, region: .regionName,
|
||||
country: .country, zip: .zip, lat: .lat, lon: .lon, tz: .timezone }
|
||||
else
|
||||
((.loc // "" | split(",")) as $l
|
||||
| { ip: .ip,
|
||||
isp: (.org // null | if . then sub("^AS[0-9]+ +"; "") else . end),
|
||||
as: .org, city: .city, region: .region, country: .country,
|
||||
zip: .postal,
|
||||
lat: ($l[0] // null), lon: ($l[1] // null), tz: .timezone })
|
||||
end) as $d
|
||||
| if $d.ip == null then "unexpected response" | halt_error(3) else
|
||||
[
|
||||
(if ($all or $ip) and $d.ip then "IP Address : \($d.ip)" else empty end),
|
||||
(if ($all or $isp) and $d.isp then "ISP : \($d.isp)" else empty end),
|
||||
(if ($all or $loc) then
|
||||
([$d.city, $d.region, $d.country] | clean | join(", ")
|
||||
| select(. != "")
|
||||
| "Location : \(.)" + (if $d.zip then " (\($d.zip))" else "" end))
|
||||
else empty end),
|
||||
(if ($all or $coord) and $d.lat and $d.lon then "Coordinates : \($d.lat), \($d.lon)" else empty end),
|
||||
(if ($all or $as) and $d.as then "AS : \($d.as)" else empty end),
|
||||
(if $all and $d.tz then "Timezone : \($d.tz)" else empty end)
|
||||
] | .[]
|
||||
end' 2>/dev/null)
|
||||
local rc=$?
|
||||
if (( rc != 0 )) || [[ -z $out ]]; then
|
||||
disp E "Unexpected answer from $api_url (use --raw to see it)."
|
||||
return 3
|
||||
fi
|
||||
printf '%s\n' "$out"
|
||||
}
|
||||
export -f myextip
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
|
||||
# EOF
|
||||
|
||||
+549
-30
@@ -1,42 +1,561 @@
|
||||
#!/usr/bin/env bash
|
||||
# ------------------------------------------------------------------------------
|
||||
# Look for a package within installed one
|
||||
# Copyright (c) 2013-2026 Geoffray Levasseur <fatalerrors@geoffray-levasseur.org>
|
||||
# Protected by the BSD3 license. Please read bellow for details.
|
||||
#
|
||||
# * Redistribution and use in source and binary forms,
|
||||
# * with or without modification, are permitted provided
|
||||
# * that the following conditions are met:
|
||||
# *
|
||||
# * Redistributions of source code must retain the above
|
||||
# * copyright notice, this list of conditions and the
|
||||
# * following disclaimer.
|
||||
# *
|
||||
# * Redistributions in binary form must reproduce the above
|
||||
# * copyright notice, this list of conditions and the following
|
||||
# * disclaimer in the documentation and/or other materials
|
||||
# * provided with the distribution.
|
||||
# *
|
||||
# * Neither the name of the copyright holder nor the names
|
||||
# * of any other contributors may be used to endorse or
|
||||
# * promote products derived from this software without
|
||||
# * specific prior written permission.
|
||||
# *
|
||||
# * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
|
||||
# * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
|
||||
# * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
|
||||
# * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
# * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
|
||||
# * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
# * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
|
||||
# * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
# * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||
# * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
|
||||
# * OF SUCH DAMAGE.
|
||||
# ------------------------------------------------------------------------------
|
||||
dpkgs ()
|
||||
|
||||
load_conf "packages"
|
||||
|
||||
# Common helpers, then the ones private to this module (helpers/packages.sh)
|
||||
# shellcheck source=../helpers/common.sh
|
||||
declare -F _profile_load_helpers >/dev/null || . "$(dirname -- "${BASH_SOURCE[0]}")/../helpers/common.sh"
|
||||
_profile_load_helpers "${BASH_SOURCE[0]}"
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# Detect the active package manager of the current distribution.
|
||||
# Detection is based on /etc/os-release (ID / ID_LIKE), then falls back to
|
||||
# checking available binaries in a fixed priority order.
|
||||
# Echoes one of: apt dnf yum zypper pacman apk portage xbps nix
|
||||
# Returns 1 if no known package manager could be identified.
|
||||
# shellcheck disable=SC2120 # only --help is a possible argument
|
||||
get_pkgmgr()
|
||||
{
|
||||
local count=0
|
||||
for opt in $@ ; do
|
||||
case $opt in
|
||||
"-h"|"--help")
|
||||
echo "dpkgs: look for an installed package by it's name."
|
||||
echo
|
||||
echo "Usage: dpkgs <string>"
|
||||
if [[ "${1:-}" == -h || "${1:-}" == --help ]]; then
|
||||
printf "get_pkgmgr: print the package manager of the running distribution.\n"
|
||||
printf "Usage: get_pkgmgr\n"
|
||||
printf "Prints one of: apt dnf yum zypper pacman apk portage xbps nix (status 1 if unknown).\n"
|
||||
return 0
|
||||
fi
|
||||
local distro_id="" distro_like=""
|
||||
if [[ -r /etc/os-release ]]; then
|
||||
# shellcheck disable=SC1091
|
||||
distro_id=$( . /etc/os-release 2>/dev/null; printf '%s' "${ID:-}" )
|
||||
# shellcheck disable=SC1091
|
||||
distro_like=$( . /etc/os-release 2>/dev/null; printf '%s' "${ID_LIKE:-}" )
|
||||
fi
|
||||
|
||||
# Map distro IDs/families to a package manager.
|
||||
# ID_LIKE is space-separated and may list multiple families.
|
||||
local id
|
||||
for id in $distro_id $distro_like; do
|
||||
case "${id,,}" in
|
||||
debian|ubuntu|linuxmint|raspbian|pop|kali|elementary|zorin|neon|parrot)
|
||||
echo "apt"
|
||||
return 0
|
||||
;;
|
||||
|
||||
"-"*)
|
||||
echo "Invalid option, use \"dpkgs --help\" to display usage."
|
||||
echo
|
||||
return 1
|
||||
fedora)
|
||||
echo "dnf"
|
||||
return 0
|
||||
;;
|
||||
|
||||
*)
|
||||
local pkg=$1 && shift
|
||||
count=$(( $count + 1 ))
|
||||
[[ $count -gt 1 ]] &&
|
||||
echo "*** Error: Please specify a package name, without space, eventually partial." &&
|
||||
return 1
|
||||
|
||||
rhel|centos|rocky|almalinux|ol|scientific|amzn)
|
||||
command -v dnf >/dev/null 2>&1 && { echo "dnf"; return 0; }
|
||||
echo "yum"
|
||||
return 0
|
||||
;;
|
||||
opensuse*|suse|sles|sled)
|
||||
echo "zypper"
|
||||
return 0
|
||||
;;
|
||||
arch|manjaro|endeavouros|garuda|artix|cachyos)
|
||||
echo "pacman"
|
||||
return 0
|
||||
;;
|
||||
alpine)
|
||||
echo "apk"
|
||||
return 0
|
||||
;;
|
||||
gentoo)
|
||||
echo "portage"
|
||||
return 0
|
||||
;;
|
||||
void)
|
||||
echo "xbps"
|
||||
return 0
|
||||
;;
|
||||
nixos)
|
||||
echo "nix"
|
||||
return 0
|
||||
;;
|
||||
esac
|
||||
done
|
||||
[[ $count -lt 1 ]] &&
|
||||
echo "*** Error: Please specify a package name, without space, eventually partial." &&
|
||||
return 1
|
||||
|
||||
[[ -x /usr/sbin/dpkg ]] &&
|
||||
echo "*** Error: dpkg command seems unavialable." &&
|
||||
return 2
|
||||
# Fallback: check for binaries in priority order.
|
||||
local bin
|
||||
for bin in apt apt-get dnf yum zypper pacman apk emerge xbps-install nix-env; do
|
||||
command -v "$bin" >/dev/null 2>&1 && {
|
||||
case "$bin" in
|
||||
apt-get) echo "apt" ;;
|
||||
emerge) echo "portage" ;;
|
||||
xbps-install) echo "xbps" ;;
|
||||
nix-env) echo "nix" ;;
|
||||
*) echo "$bin" ;;
|
||||
esac
|
||||
return 0
|
||||
}
|
||||
done
|
||||
|
||||
dpkg -l | grep $pkg
|
||||
return 1
|
||||
}
|
||||
export -f dpkgs
|
||||
export -f get_pkgmgr
|
||||
# ------------------------------------------------------------------------------
|
||||
# Look for a package within installed one
|
||||
# Usage: pkgs <string>
|
||||
pkgs()
|
||||
{
|
||||
local ignore_case=${PKGS_DEFAULT_IGNORE_CASE:-0}
|
||||
|
||||
local PARSED
|
||||
PARSED=$(getopt -o hi --long help,ignore-case -n 'pkgs' -- "$@")
|
||||
# shellcheck disable=SC2181 # getopt return code is checked immediately after
|
||||
if [[ $? -ne 0 ]]; then
|
||||
disp E "Invalid options, use \"pkgs --help\" to display usage."
|
||||
return 1
|
||||
fi
|
||||
local -a _pargs=()
|
||||
_getopt_split _pargs "$PARSED"
|
||||
set -- "${_pargs[@]}"
|
||||
|
||||
while true; do
|
||||
case "$1" in
|
||||
-h|--help)
|
||||
printf "pkgs: Look for an installed package by its name.\n\n"
|
||||
printf "Usage: pkgs [options] <string>\n\n"
|
||||
printf "Options:\n"
|
||||
printf "\t-h, --help\tDisplay this help screen\n"
|
||||
printf "\t-i, --ignore-case\tIgnore case distinctions\n"
|
||||
return 0
|
||||
;;
|
||||
-i|--ignore-case)
|
||||
ignore_case=1
|
||||
shift
|
||||
;;
|
||||
--)
|
||||
shift
|
||||
break
|
||||
;;
|
||||
*)
|
||||
disp E "Invalid option: $1"
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
local pkg="$1"
|
||||
[[ -z "$pkg" ]] && {
|
||||
disp E "Please specify a package name, without space, eventually partial."
|
||||
return 1
|
||||
}
|
||||
|
||||
# Build grep command
|
||||
local grep_opt=""
|
||||
(( ignore_case )) && grep_opt="-i"
|
||||
|
||||
local pkgmgr
|
||||
pkgmgr=$(get_pkgmgr) || {
|
||||
disp E "No usable package manager could be detected on this system."
|
||||
return 2
|
||||
}
|
||||
|
||||
local -a list_cmd
|
||||
case "$pkgmgr" in
|
||||
apt) list_cmd=(dpkg-query -l) ;;
|
||||
dnf|yum|zypper) list_cmd=(rpm -qa) ;;
|
||||
pacman) list_cmd=(pacman -Q) ;;
|
||||
apk) list_cmd=(apk list --installed) ;;
|
||||
portage) list_cmd=(qlist -I) ;;
|
||||
xbps) list_cmd=(xbps-query -l) ;;
|
||||
nix) list_cmd=(nix-env -q) ;;
|
||||
*)
|
||||
disp E "Package manager '$pkgmgr' is not supported by pkgs."
|
||||
return 2
|
||||
;;
|
||||
esac
|
||||
|
||||
# -e: a pattern starting with "-" must not be read as a grep option
|
||||
"${list_cmd[@]}" | grep ${grep_opt:+"$grep_opt"} -e "$pkg"
|
||||
}
|
||||
export -f pkgs
|
||||
# ------------------------------------------------------------------------------
|
||||
# Find which installed package a file belongs to.
|
||||
# Usage: pkgf <file>
|
||||
pkgf()
|
||||
{
|
||||
local PARSED
|
||||
PARSED=$(getopt -o h --long help -n 'pkgf' -- "$@")
|
||||
# shellcheck disable=SC2181
|
||||
if [[ $? -ne 0 ]]; then
|
||||
disp E "Invalid options, use \"pkgf --help\" to display usage."
|
||||
return 1
|
||||
fi
|
||||
local -a _pargs=()
|
||||
_getopt_split _pargs "$PARSED"
|
||||
set -- "${_pargs[@]}"
|
||||
|
||||
while true; do
|
||||
case "$1" in
|
||||
-h|--help)
|
||||
printf "pkgf: Find which installed package owns a given file.\n\n"
|
||||
printf "Usage: pkgf [options] <file>\n\n"
|
||||
printf "Options:\n"
|
||||
printf "\t-h, --help\tDisplay this help screen\n"
|
||||
return 0
|
||||
;;
|
||||
--)
|
||||
shift
|
||||
break
|
||||
;;
|
||||
*)
|
||||
disp E "Invalid option: $1"
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
local file="$1"
|
||||
[[ -z "$file" ]] && {
|
||||
disp E "Please specify a file path."
|
||||
return 1
|
||||
}
|
||||
# A bare command name that is not a file here ("pkgf ls") is looked up in
|
||||
# the PATH: the package managers expect a path, and would otherwise match
|
||||
# the name as a pattern against every file of every package.
|
||||
if [[ $file != */* && ! -e $file ]]; then
|
||||
local _found
|
||||
_found=$(command -v -- "$file" 2>/dev/null) && [[ $_found == /* ]] && file="$_found"
|
||||
fi
|
||||
# "./" keeps a name starting with "-" from being read as an option
|
||||
[[ $file == -* ]] && file="./$file"
|
||||
|
||||
local pkgmgr
|
||||
pkgmgr=$(get_pkgmgr) || {
|
||||
disp E "No usable package manager could be detected on this system."
|
||||
return 2
|
||||
}
|
||||
|
||||
case "$pkgmgr" in
|
||||
apt)
|
||||
dpkg -S "$file"
|
||||
;;
|
||||
dnf|yum|zypper)
|
||||
rpm -qf "$file"
|
||||
;;
|
||||
pacman)
|
||||
pacman -Qo "$file"
|
||||
;;
|
||||
apk)
|
||||
apk info --who-owns "$file"
|
||||
;;
|
||||
portage)
|
||||
qfile "$file"
|
||||
;;
|
||||
xbps)
|
||||
xbps-query -o "$file"
|
||||
;;
|
||||
nix)
|
||||
nix-locate "$file"
|
||||
;;
|
||||
*)
|
||||
disp E "Package manager '$pkgmgr' is not supported by pkgf."
|
||||
return 2
|
||||
;;
|
||||
esac
|
||||
}
|
||||
export -f pkgf
|
||||
# ------------------------------------------------------------------------------
|
||||
# Install packages with the package manager of the distribution.
|
||||
# Usage: pkgi [options] <package> [package...]
|
||||
pkgi()
|
||||
{
|
||||
local assume_yes=0 dry_run=0
|
||||
|
||||
local PARSED
|
||||
PARSED=$(getopt -o hyn --long help,yes,dry-run -n 'pkgi' -- "$@")
|
||||
# shellcheck disable=SC2181 # getopt return code is checked immediately after
|
||||
if [[ $? -ne 0 ]]; then
|
||||
disp E "Invalid options, use \"pkgi --help\" to display usage."
|
||||
return 1
|
||||
fi
|
||||
# Restore the getopt-normalised (shell-quoted) argument list
|
||||
local -a _pargs=()
|
||||
_getopt_split _pargs "$PARSED"
|
||||
set -- "${_pargs[@]}"
|
||||
|
||||
while true; do
|
||||
case "$1" in
|
||||
-h|--help)
|
||||
printf "pkgi: Install packages (distro-aware via get_pkgmgr).\n\n"
|
||||
printf "Usage: pkgi [options] <package> [package...]\n\n"
|
||||
printf "Options:\n"
|
||||
printf "\t-h, --help\tDisplay this help screen\n"
|
||||
printf "\t-y, --yes\tDo not ask for confirmation (where the manager asks)\n"
|
||||
printf "\t-n, --dry-run\tOnly display the command that would be run\n\n"
|
||||
printf "Root privileges are obtained with sudo (or doas) when needed.\n"
|
||||
return 0
|
||||
;;
|
||||
-y|--yes)
|
||||
assume_yes=1
|
||||
shift
|
||||
;;
|
||||
-n|--dry-run)
|
||||
dry_run=1
|
||||
shift
|
||||
;;
|
||||
--)
|
||||
shift
|
||||
break
|
||||
;;
|
||||
*)
|
||||
disp E "Invalid option: $1"
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ $# -eq 0 ]]; then
|
||||
disp E "Please specify at least one package name."
|
||||
return 1
|
||||
fi
|
||||
local pkg
|
||||
for pkg in "$@"; do
|
||||
# Not passed after "--": not every manager understands it
|
||||
if [[ $pkg == -* ]]; then
|
||||
disp E "Invalid package name: '$pkg'."
|
||||
return 1
|
||||
fi
|
||||
done
|
||||
|
||||
local pkgmgr
|
||||
pkgmgr=$(get_pkgmgr) || {
|
||||
disp E "No usable package manager could be detected on this system."
|
||||
return 2
|
||||
}
|
||||
|
||||
local -a cmd=()
|
||||
local needs_root=1
|
||||
case "$pkgmgr" in
|
||||
apt)
|
||||
cmd=(apt-get install)
|
||||
(( assume_yes )) && cmd+=(-y)
|
||||
;;
|
||||
dnf|yum)
|
||||
cmd=("$pkgmgr" install)
|
||||
(( assume_yes )) && cmd+=(-y)
|
||||
;;
|
||||
zypper)
|
||||
cmd=(zypper install)
|
||||
(( assume_yes )) && cmd+=(-y)
|
||||
;;
|
||||
pacman)
|
||||
cmd=(pacman -S)
|
||||
(( assume_yes )) && cmd+=(--noconfirm)
|
||||
;;
|
||||
apk)
|
||||
# apk add never asks for confirmation
|
||||
cmd=(apk add)
|
||||
;;
|
||||
portage)
|
||||
cmd=(emerge)
|
||||
(( assume_yes )) || cmd+=(--ask)
|
||||
;;
|
||||
xbps)
|
||||
cmd=(xbps-install -S)
|
||||
(( assume_yes )) && cmd+=(-y)
|
||||
;;
|
||||
nix)
|
||||
# nix-env works on the user's profile: no root needed
|
||||
cmd=(nix-env -i)
|
||||
needs_root=0
|
||||
;;
|
||||
*)
|
||||
disp E "Package manager '$pkgmgr' is not supported by pkgi."
|
||||
return 2
|
||||
;;
|
||||
esac
|
||||
|
||||
_pkg_run "$needs_root" "$dry_run" "${cmd[@]}" "$@"
|
||||
}
|
||||
export -f pkgi
|
||||
# ------------------------------------------------------------------------------
|
||||
# Uninstall packages with the package manager of the distribution.
|
||||
# Usage: pkgr [options] <package> [package...]
|
||||
# -p, --purge also delete the configuration files (apt purge, pacman -n,
|
||||
# apk --purge); managers without such a mode just remove
|
||||
# -d, --deps also remove the dependencies no longer needed. Without it,
|
||||
# the managers that do it by default (dnf, yum) are told not to
|
||||
# (apk always cleans up: it has no way to prevent it)
|
||||
pkgr()
|
||||
{
|
||||
local purge=0 deps=0 assume_yes=0 dry_run=0
|
||||
|
||||
local PARSED
|
||||
PARSED=$(getopt -o hpdyn --long help,purge,deps,yes,dry-run -n 'pkgr' -- "$@")
|
||||
# shellcheck disable=SC2181 # getopt return code is checked immediately after
|
||||
if [[ $? -ne 0 ]]; then
|
||||
disp E "Invalid options, use \"pkgr --help\" to display usage."
|
||||
return 1
|
||||
fi
|
||||
# Restore the getopt-normalised (shell-quoted) argument list
|
||||
local -a _pargs=()
|
||||
_getopt_split _pargs "$PARSED"
|
||||
set -- "${_pargs[@]}"
|
||||
|
||||
while true; do
|
||||
case "$1" in
|
||||
-h|--help)
|
||||
printf "pkgr: Uninstall packages (distro-aware via get_pkgmgr).\n\n"
|
||||
printf "Usage: pkgr [options] <package> [package...]\n\n"
|
||||
printf "Options:\n"
|
||||
printf "\t-h, --help\tDisplay this help screen\n"
|
||||
printf "\t-p, --purge\tAlso delete configuration files (apt, pacman, apk)\n"
|
||||
printf "\t-d, --deps\tAlso remove dependencies that are no longer needed\n"
|
||||
printf "\t-y, --yes\tDo not ask for confirmation (where the manager asks)\n"
|
||||
printf "\t-n, --dry-run\tOnly display the command that would be run\n\n"
|
||||
printf "Root privileges are obtained with sudo (or doas) when needed.\n"
|
||||
return 0
|
||||
;;
|
||||
-p|--purge)
|
||||
purge=1
|
||||
shift
|
||||
;;
|
||||
-d|--deps)
|
||||
deps=1
|
||||
shift
|
||||
;;
|
||||
-y|--yes)
|
||||
assume_yes=1
|
||||
shift
|
||||
;;
|
||||
-n|--dry-run)
|
||||
dry_run=1
|
||||
shift
|
||||
;;
|
||||
--)
|
||||
shift
|
||||
break
|
||||
;;
|
||||
*)
|
||||
disp E "Invalid option: $1"
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ $# -eq 0 ]]; then
|
||||
disp E "Please specify at least one package name."
|
||||
return 1
|
||||
fi
|
||||
local pkg
|
||||
for pkg in "$@"; do
|
||||
if [[ $pkg == -* ]]; then
|
||||
disp E "Invalid package name: '$pkg'."
|
||||
return 1
|
||||
fi
|
||||
done
|
||||
|
||||
local pkgmgr
|
||||
pkgmgr=$(get_pkgmgr) || {
|
||||
disp E "No usable package manager could be detected on this system."
|
||||
return 2
|
||||
}
|
||||
|
||||
local -a cmd=() post=()
|
||||
local needs_root=1 purge_done=0
|
||||
case "$pkgmgr" in
|
||||
apt)
|
||||
# --auto-remove also drops the automatically installed packages
|
||||
# nothing needs any more
|
||||
if (( purge )); then cmd=(apt-get purge); purge_done=1; else cmd=(apt-get remove); fi
|
||||
(( deps )) && cmd+=(--auto-remove)
|
||||
(( assume_yes )) && cmd+=(-y)
|
||||
;;
|
||||
dnf|yum)
|
||||
cmd=("$pkgmgr" remove)
|
||||
# These managers remove the unneeded dependencies by default
|
||||
(( deps )) || cmd+=(--setopt=clean_requirements_on_remove=False)
|
||||
(( assume_yes )) && cmd+=(-y)
|
||||
;;
|
||||
zypper)
|
||||
cmd=(zypper remove)
|
||||
(( deps )) && cmd+=(--clean-deps)
|
||||
(( assume_yes )) && cmd+=(-y)
|
||||
;;
|
||||
pacman)
|
||||
# -n: no .pacsave backups of the configuration files;
|
||||
# -s: also remove dependencies nothing else needs
|
||||
local flags="-R"
|
||||
(( purge )) && { flags+="n"; purge_done=1; }
|
||||
(( deps )) && flags+="s"
|
||||
cmd=(pacman "$flags")
|
||||
(( assume_yes )) && cmd+=(--noconfirm)
|
||||
;;
|
||||
apk)
|
||||
cmd=(apk del)
|
||||
(( purge )) && { cmd+=(--purge); purge_done=1; }
|
||||
;;
|
||||
portage)
|
||||
cmd=(emerge --unmerge)
|
||||
(( assume_yes )) || cmd+=(--ask)
|
||||
# depclean removes everything no longer needed, as autoremove does
|
||||
(( deps )) && post=(emerge --depclean)
|
||||
;;
|
||||
xbps)
|
||||
cmd=(xbps-remove)
|
||||
(( deps )) && cmd+=(-R)
|
||||
(( assume_yes )) && cmd+=(-y)
|
||||
;;
|
||||
nix)
|
||||
cmd=(nix-env -e)
|
||||
needs_root=0
|
||||
# Dependencies live in the store: collect what is now unreachable
|
||||
(( deps )) && post=(nix-collect-garbage)
|
||||
;;
|
||||
*)
|
||||
disp E "Package manager '$pkgmgr' is not supported by pkgr."
|
||||
return 2
|
||||
;;
|
||||
esac
|
||||
|
||||
if (( purge && ! purge_done )); then
|
||||
disp W "$pkgmgr has no purge mode: the packages are removed normally."
|
||||
fi
|
||||
|
||||
_pkg_run "$needs_root" "$dry_run" "${cmd[@]}" "$@" || return $?
|
||||
if (( ${#post[@]} )); then
|
||||
# Only reached when the removal itself succeeded
|
||||
_pkg_run "$needs_root" "$dry_run" "${post[@]}"
|
||||
fi
|
||||
}
|
||||
export -f pkgr
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
# EOF
|
||||
|
||||
+493
-20
@@ -1,40 +1,513 @@
|
||||
#!/usr/bin/env bash
|
||||
# ------------------------------------------------------------------------------
|
||||
# Copyright (c) 2013-2026 Geoffray Levasseur <fatalerrors@geoffray-levasseur.org>
|
||||
# Protected by the BSD3 license. Please read bellow for details.
|
||||
#
|
||||
# * Redistribution and use in source and binary forms,
|
||||
# * with or without modification, are permitted provided
|
||||
# * that the following conditions are met:
|
||||
# *
|
||||
# * Redistributions of source code must retain the above
|
||||
# * copyright notice, this list of conditions and the
|
||||
# * following disclaimer.
|
||||
# *
|
||||
# * Redistributions in binary form must reproduce the above
|
||||
# * copyright notice, this list of conditions and the following
|
||||
# * disclaimer in the documentation and/or other materials
|
||||
# * provided with the distribution.
|
||||
# *
|
||||
# * Neither the name of the copyright holder nor the names
|
||||
# * of any other contributors may be used to endorse or
|
||||
# * promote products derived from this software without
|
||||
# * specific prior written permission.
|
||||
# *
|
||||
# * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
|
||||
# * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
|
||||
# * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
|
||||
# * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
# * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
|
||||
# * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
# * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
|
||||
# * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
# * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||
# * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
|
||||
# * OF SUCH DAMAGE.
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
load_conf "processes"
|
||||
|
||||
# Common helpers, then the ones private to this module (helpers/processes.sh)
|
||||
# shellcheck source=../helpers/common.sh
|
||||
declare -F _profile_load_helpers >/dev/null || . "$(dirname -- "${BASH_SOURCE[0]}")/../helpers/common.sh"
|
||||
_profile_load_helpers "${BASH_SOURCE[0]}"
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# Search processes matching the given string
|
||||
# ------------------------------------------------------------------------------
|
||||
ppg ()
|
||||
# Usage: ppg <string>
|
||||
ppg()
|
||||
{
|
||||
ps -edf | grep $@ | grep -v "grep $@"
|
||||
local PARSED
|
||||
|
||||
PARSED=$(getopt -o h --long help -n 'ppg' -- "$@")
|
||||
# shellcheck disable=SC2181 # getopt return code is checked immediately after
|
||||
if [[ $? -ne 0 ]]; then
|
||||
disp E "Invalid options, use \"ppg --help\" to display usage."
|
||||
return 1
|
||||
fi
|
||||
|
||||
local -a _pargs=()
|
||||
_getopt_split _pargs "$PARSED"
|
||||
set -- "${_pargs[@]}"
|
||||
while true; do
|
||||
case "$1" in
|
||||
-h|--help)
|
||||
printf "ppg: Search processes matching the given string.\n\n"
|
||||
printf "Usage: ppg <string>\n\n"
|
||||
printf "Options:\n"
|
||||
printf "\t-h, --help\t\tDisplay this help screen\n"
|
||||
return 0
|
||||
;;
|
||||
--)
|
||||
shift
|
||||
break
|
||||
;;
|
||||
*)
|
||||
disp E "Invalid options, use \"ppg --help\" to display usage."
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ -z "$1" ]]; then
|
||||
disp E "Usage: ppg <string>"
|
||||
return 1
|
||||
fi
|
||||
|
||||
local pattern="$*"
|
||||
|
||||
# pgrep reads the pattern as an extended regular expression, the fallback
|
||||
# below as a plain substring. Passed through ENVIRON, not awk -v: besides
|
||||
# escape handling, a pattern on awk's own command line would match the awk
|
||||
# process in the ps listing.
|
||||
if command -v pgrep >/dev/null 2>&1; then
|
||||
pgrep -af -- "$pattern"
|
||||
return $?
|
||||
fi
|
||||
|
||||
ps -ef | PPG_PATTERN="$pattern" awk '
|
||||
BEGIN { pattern = ENVIRON["PPG_PATTERN"] }
|
||||
NR == 1 {
|
||||
print
|
||||
next
|
||||
}
|
||||
index($0, pattern) {
|
||||
print
|
||||
matched = 1
|
||||
}
|
||||
END {
|
||||
exit matched ? 0 : 1
|
||||
}
|
||||
'
|
||||
}
|
||||
export -f ppg
|
||||
# ------------------------------------------------------------------------------
|
||||
# List processes owned by a specific user
|
||||
# Usage: ppu <username>
|
||||
ppu()
|
||||
{
|
||||
local PARSED
|
||||
|
||||
PARSED=$(getopt -o h --long help -n 'ppu' -- "$@")
|
||||
# shellcheck disable=SC2181 # getopt return code is checked immediately after
|
||||
if [[ $? -ne 0 ]]; then
|
||||
disp E "Invalid options, use \"ppu --help\" to display usage."
|
||||
return 1
|
||||
fi
|
||||
|
||||
local -a _pargs=()
|
||||
_getopt_split _pargs "$PARSED"
|
||||
set -- "${_pargs[@]}"
|
||||
while true; do
|
||||
case "$1" in
|
||||
-h|--help)
|
||||
printf "ppu: List processes owned by a specific user.\n\n"
|
||||
printf "Usage: ppu <username>\n\n"
|
||||
printf "Options:\n"
|
||||
printf "\t-h, --help\t\tDisplay this help screen\n"
|
||||
return 0
|
||||
;;
|
||||
--)
|
||||
shift
|
||||
break
|
||||
;;
|
||||
*)
|
||||
disp E "Invalid options, use \"ppu --help\" to display usage."
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ -z "$1" ]]; then
|
||||
disp E "Usage: ppu <username>"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# -u lists processes for a specific user
|
||||
# -o provides a clean, standard output format
|
||||
ps -u "$1" -o "${PPU_DEFAULT_FORMAT:-pid,user,%cpu,%mem,start,time,command}"
|
||||
}
|
||||
export -f ppu
|
||||
# ------------------------------------------------------------------------------
|
||||
# List processes by exact command name (no path/parameters)
|
||||
# Usage: ppn <command_name>
|
||||
ppn()
|
||||
{
|
||||
local PARSED
|
||||
|
||||
PARSED=$(getopt -o h --long help -n 'ppn' -- "$@")
|
||||
# shellcheck disable=SC2181 # getopt return code is checked immediately after
|
||||
if [[ $? -ne 0 ]]; then
|
||||
disp E "Invalid options, use \"ppn --help\" to display usage."
|
||||
return 1
|
||||
fi
|
||||
|
||||
local -a _pargs=()
|
||||
_getopt_split _pargs "$PARSED"
|
||||
set -- "${_pargs[@]}"
|
||||
while true; do
|
||||
case "$1" in
|
||||
-h|--help)
|
||||
printf "ppn: List processes by exact command name (no path/parameters).\n\n"
|
||||
printf "Usage: ppn <command_name>\n\n"
|
||||
printf "Options:\n"
|
||||
printf "\t-h, --help\t\tDisplay this help screen\n"
|
||||
return 0
|
||||
;;
|
||||
--)
|
||||
shift
|
||||
break
|
||||
;;
|
||||
*)
|
||||
disp E "Invalid options, use \"ppn --help\" to display usage."
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ -z "$1" ]]; then
|
||||
disp E "Usage: ppn <command_name>"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# ps prints the PID and the command name (comm, 15 characters at most, no
|
||||
# arguments); awk keeps the header and the lines whose whole name equals
|
||||
# the argument. grep -w was wrong: it also matched inside the PID column,
|
||||
# and in names such as "kde-bash" ("-" is not a word character).
|
||||
ps -eo pid,comm | PPN_NAME="$1" awk '
|
||||
BEGIN { name = ENVIRON["PPN_NAME"] }
|
||||
NR == 1 { print; next }
|
||||
{
|
||||
line = $0
|
||||
sub(/^ *[0-9]+ +/, "", line)
|
||||
if (line == name) { print; found = 1 }
|
||||
}
|
||||
END { exit found ? 0 : 1 }
|
||||
'
|
||||
}
|
||||
export -f ppn
|
||||
# ------------------------------------------------------------------------------
|
||||
# Get PID list of the given process name
|
||||
# ------------------------------------------------------------------------------
|
||||
gpid ()
|
||||
# Usage: gpid <process_name [process_name2 ...]>
|
||||
gpid()
|
||||
{
|
||||
[[ $UID -eq 0 ]] && local psopt="-A"
|
||||
[[ $# -eq 1 ]] && local single=1
|
||||
for pid in $@; do
|
||||
local result=$(ps $psopt | grep $pid | awk '{print $1}' | sed "s/\n/ /")
|
||||
if [[ $single ]]; then
|
||||
[[ $result ]] && echo "${result//$'\n'/ }"
|
||||
local PARSED
|
||||
|
||||
PARSED=$(getopt -o h --long help -n 'gpid' -- "$@")
|
||||
# shellcheck disable=SC2181 # getopt return code is checked immediately after
|
||||
if [[ $? -ne 0 ]]; then
|
||||
disp E "Invalid options, use \"gpid --help\" to display usage."
|
||||
return 1
|
||||
fi
|
||||
|
||||
local -a _pargs=()
|
||||
_getopt_split _pargs "$PARSED"
|
||||
set -- "${_pargs[@]}"
|
||||
while true; do
|
||||
case "$1" in
|
||||
-h|--help)
|
||||
printf "gpid: Get PID list of the given process name.\n\n"
|
||||
printf "Usage: gpid <process_name [process_name2 ...]>\n\n"
|
||||
printf "Options:\n"
|
||||
printf "\t-h, --help\t\tDisplay this help screen\n"
|
||||
return 0
|
||||
;;
|
||||
--)
|
||||
shift
|
||||
break
|
||||
;;
|
||||
*)
|
||||
disp E "Invalid options, use \"gpid --help\" to display usage."
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ -z "$1" ]]; then
|
||||
disp E "Usage: gpid <process_name [process_name2 ...]>"
|
||||
return 1
|
||||
fi
|
||||
local single=0
|
||||
local found=0
|
||||
local proc_name result
|
||||
|
||||
[[ $# -eq 1 ]] && single=1
|
||||
|
||||
for proc_name in "$@"; do
|
||||
result=""
|
||||
|
||||
if command -v pgrep >/dev/null 2>&1; then
|
||||
result=$(pgrep -d ' ' -x -- "$proc_name")
|
||||
else
|
||||
[[ $result ]] && echo "$pid: ${result//$'\n'/ }"
|
||||
result=$(ps -eo pid=,comm= | GPID_NAME="$proc_name" awk '
|
||||
BEGIN { proc = ENVIRON["GPID_NAME"] }
|
||||
$2 == proc {
|
||||
if (out != "") {
|
||||
out = out " "
|
||||
}
|
||||
out = out $1
|
||||
}
|
||||
END {
|
||||
print out
|
||||
}
|
||||
')
|
||||
fi
|
||||
|
||||
[[ -z "$result" ]] && continue
|
||||
|
||||
found=1
|
||||
if (( single )); then
|
||||
echo "$result"
|
||||
else
|
||||
echo "$proc_name: $result"
|
||||
fi
|
||||
done
|
||||
[[ $result ]] || return 1
|
||||
|
||||
(( found )) || return 1
|
||||
}
|
||||
export -f gpid
|
||||
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# Kill all processes owned by the given users
|
||||
# ------------------------------------------------------------------------------
|
||||
ku ()
|
||||
# Kill all processes owned by the given users (kill user)
|
||||
# Usage: ku [options] <username1 [username2 ...]>
|
||||
# The users are validated first: nothing is killed if one of them is unknown.
|
||||
# The processes of root and of the current user are never killed (it would take
|
||||
# the session down), except in dry-run mode.
|
||||
ku()
|
||||
{
|
||||
for u in $@; do
|
||||
killall -u $u
|
||||
local dry_run=0
|
||||
local -a signal_opt=() _ku_sigs=() _ku_rest=()
|
||||
|
||||
_proc_split_signals _ku_sigs _ku_rest "$@"
|
||||
|
||||
local PARSED
|
||||
PARSED=$(getopt -o hns: --long help,dry-run,signal: -n 'ku' -- "${_ku_rest[@]}")
|
||||
# shellcheck disable=SC2181 # getopt return code is checked immediately after
|
||||
if [[ $? -ne 0 ]]; then
|
||||
disp E "Invalid options, use \"ku --help\" to display usage."
|
||||
return 1
|
||||
fi
|
||||
# Restore the getopt-normalised (shell-quoted) argument list
|
||||
local -a _pargs=()
|
||||
_getopt_split _pargs "$PARSED"
|
||||
set -- "${_pargs[@]}"
|
||||
while true; do
|
||||
case "$1" in
|
||||
-h|--help)
|
||||
printf "ku: Kill all processes owned by the given users.\n\n"
|
||||
printf "Usage: ku [options] <username1 [username2 ...]>\n\n"
|
||||
printf "Options:\n"
|
||||
printf "\t-h, --help\t\tDisplay this help screen\n"
|
||||
printf "\t-n, --dry-run\t\tDisplay commands without executing them\n"
|
||||
printf "\t-s, --signal SIG\tSignal to send (overrides KU_DEFAULT_SIGNAL)\n"
|
||||
printf "\t --signal=SIG\tSame as above\n"
|
||||
printf "\t -SIG / -NUM\t\tSignal format compatible with kill\n\n"
|
||||
printf "Users are given by name or numeric ID. The processes of root and of the\n"
|
||||
printf "current user are never killed (except with --dry-run).\n"
|
||||
return 0
|
||||
;;
|
||||
-n|--dry-run)
|
||||
dry_run=1
|
||||
shift
|
||||
;;
|
||||
-s|--signal)
|
||||
signal_opt=(-s "$2")
|
||||
shift 2
|
||||
;;
|
||||
--)
|
||||
shift
|
||||
break
|
||||
;;
|
||||
*)
|
||||
disp E "Invalid options, use \"ku --help\" to display usage."
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
# A kill-style signal (-9, -KILL) applies unless -s/--signal was given
|
||||
if [[ ${#signal_opt[@]} -eq 0 && ${#_ku_sigs[@]} -gt 0 ]]; then
|
||||
signal_opt=("${_ku_sigs[-1]}")
|
||||
fi
|
||||
|
||||
if [[ $# -eq 0 ]]; then
|
||||
disp E "Usage: ku [options] <username1 [username2 ...]>"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# First pass: resolve every user to a name (killall only takes names, so
|
||||
# numeric IDs are translated) and check it, before anything is killed.
|
||||
local u uid uname
|
||||
local -a unames=()
|
||||
for u in "$@"; do
|
||||
if ! uid=$(id -u -- "$u" 2>/dev/null); then
|
||||
disp E "User '$u' does not exist."
|
||||
return 1
|
||||
fi
|
||||
uname=$(id -un -- "$u")
|
||||
if (( ! dry_run )) && [[ $uid -eq 0 || $uid -eq $EUID ]]; then
|
||||
disp E "Safety abort: refusing to kill the processes of '$uname' (root or current user)."
|
||||
return 1
|
||||
fi
|
||||
unames+=("$uname")
|
||||
done
|
||||
|
||||
# Second pass: kill. The failure of one user does not skip the others.
|
||||
local rc=0
|
||||
local -a cmd
|
||||
for uname in "${unames[@]}"; do
|
||||
# killall (psmisc) preferred; fall back to pkill (procps-ng).
|
||||
if command -v killall >/dev/null 2>&1; then
|
||||
cmd=(killall)
|
||||
if [[ ${#signal_opt[@]} -gt 0 ]]; then
|
||||
cmd+=("${signal_opt[@]}")
|
||||
elif [[ -n "${KU_DEFAULT_SIGNAL:-}" ]]; then
|
||||
cmd+=("-${KU_DEFAULT_SIGNAL}")
|
||||
fi
|
||||
cmd+=(-u "$uname")
|
||||
elif command -v pkill >/dev/null 2>&1; then
|
||||
cmd=(pkill)
|
||||
# Translate killall's -s SIGNAME form to pkill's -SIGNAME form.
|
||||
if [[ ${#signal_opt[@]} -eq 2 && "${signal_opt[0]}" == "-s" ]]; then
|
||||
cmd+=("-${signal_opt[1]}")
|
||||
elif [[ ${#signal_opt[@]} -gt 0 ]]; then
|
||||
cmd+=("${signal_opt[@]}")
|
||||
elif [[ -n "${KU_DEFAULT_SIGNAL:-}" ]]; then
|
||||
cmd+=("-${KU_DEFAULT_SIGNAL}")
|
||||
fi
|
||||
cmd+=(-u "$uname")
|
||||
else
|
||||
disp E "ku: neither 'killall' (psmisc) nor 'pkill' (procps) is available."
|
||||
return 1
|
||||
fi
|
||||
|
||||
if (( dry_run )); then
|
||||
printf "DRY-RUN: "
|
||||
printf "%q " "${cmd[@]}"
|
||||
printf "\n"
|
||||
else
|
||||
"${cmd[@]}" || rc=1
|
||||
fi
|
||||
done
|
||||
return "$rc"
|
||||
}
|
||||
export -f ku
|
||||
# ------------------------------------------------------------------------------
|
||||
# Kill all children of a process then the process (kill tree)
|
||||
# Usage: kt [options] <pid>
|
||||
kt()
|
||||
{
|
||||
local dry_run=0
|
||||
local -a kill_args=() _kt_sigs=() _kt_rest=()
|
||||
|
||||
_proc_split_signals _kt_sigs _kt_rest "$@"
|
||||
|
||||
local PARSED
|
||||
PARSED=$(getopt -o hns: --long help,dry-run,signal: -n 'kt' -- "${_kt_rest[@]}")
|
||||
# shellcheck disable=SC2181 # getopt return code is checked immediately after
|
||||
if [[ $? -ne 0 ]]; then
|
||||
disp E "Invalid options, use \"kt --help\" to display usage."
|
||||
return 1
|
||||
fi
|
||||
# Restore the getopt-normalised (shell-quoted) argument list
|
||||
local -a _pargs=()
|
||||
_getopt_split _pargs "$PARSED"
|
||||
set -- "${_pargs[@]}"
|
||||
while true; do
|
||||
case "$1" in
|
||||
-h|--help)
|
||||
printf "kt: Kill all children of a process then the process (kill tree).\n\n"
|
||||
printf "Usage: kt [options] <pid>\n\n"
|
||||
printf "Options:\n"
|
||||
printf "\t-h, --help\t\tDisplay this help screen\n"
|
||||
printf "\t-n, --dry-run\t\tDisplay kill commands without executing them\n"
|
||||
printf "\t-s, --signal SIG\tSignal to send to process tree\n"
|
||||
printf "\t --signal=SIG\tSame as above\n"
|
||||
printf "\t -SIG / -NUM\t\tSignal format compatible with kill\n"
|
||||
return 0
|
||||
;;
|
||||
-n|--dry-run)
|
||||
dry_run=1
|
||||
shift
|
||||
;;
|
||||
-s|--signal)
|
||||
kill_args=(-s "$2")
|
||||
shift 2
|
||||
;;
|
||||
--)
|
||||
shift
|
||||
break
|
||||
;;
|
||||
*)
|
||||
disp E "Invalid options, use \"kt --help\" to display usage."
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
# A kill-style signal (-9, -KILL) applies unless -s/--signal was given
|
||||
if [[ ${#kill_args[@]} -eq 0 && ${#_kt_sigs[@]} -gt 0 ]]; then
|
||||
kill_args=("${_kt_sigs[-1]}")
|
||||
fi
|
||||
|
||||
if [[ $# -eq 0 ]]; then
|
||||
disp E "Usage: kt [options] <pid>"
|
||||
return 1
|
||||
fi
|
||||
if [[ $# -gt 1 ]]; then
|
||||
disp E "Unexpected argument: $2. Usage: kt [options] <pid>"
|
||||
return 1
|
||||
fi
|
||||
|
||||
local parent_pid="$1"
|
||||
if [[ ! $parent_pid =~ ^[0-9]+$ ]]; then
|
||||
disp E "Invalid PID: '$parent_pid'."
|
||||
return 1
|
||||
fi
|
||||
if [[ "$parent_pid" == "0" || "$parent_pid" == "1" ]]; then
|
||||
disp E "Safety abort: Refusing to kill PID $parent_pid (system critical)."
|
||||
return 1
|
||||
fi
|
||||
if [[ "$parent_pid" == "$$" || "$parent_pid" == "$BASHPID" ]]; then
|
||||
disp E "Safety abort: Refusing to kill the current shell (PID $parent_pid)."
|
||||
return 1
|
||||
fi
|
||||
if ! command -v pgrep >/dev/null 2>&1; then
|
||||
disp E "kt: 'pgrep' (procps) is required to find the children."
|
||||
return 1
|
||||
fi
|
||||
|
||||
_kt_tree "$dry_run" "$parent_pid" "${kill_args[@]}"
|
||||
}
|
||||
export -f kt
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
# EOF
|
||||
|
||||
+428
-53
@@ -1,19 +1,335 @@
|
||||
#!/usr/bin/env bash
|
||||
# ------------------------------------------------------------------------------
|
||||
# timer_* functions : internal timing function for prompt
|
||||
# Copyright (c) 2013-2026 Geoffray Levasseur <fatalerrors@geoffray-levasseur.org>
|
||||
# Protected by the BSD3 license. Please read bellow for details.
|
||||
#
|
||||
# * Redistribution and use in source and binary forms,
|
||||
# * with or without modification, are permitted provided
|
||||
# * that the following conditions are met:
|
||||
# *
|
||||
# * Redistributions of source code must retain the above
|
||||
# * copyright notice, this list of conditions and the
|
||||
# * following disclaimer.
|
||||
# *
|
||||
# * Redistributions in binary form must reproduce the above
|
||||
# * copyright notice, this list of conditions and the following
|
||||
# * disclaimer in the documentation and/or other materials
|
||||
# * provided with the distribution.
|
||||
# *
|
||||
# * Neither the name of the copyright holder nor the names
|
||||
# * of any other contributors may be used to endorse or
|
||||
# * promote products derived from this software without
|
||||
# * specific prior written permission.
|
||||
# *
|
||||
# * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
|
||||
# * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
|
||||
# * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
|
||||
# * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
# * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
|
||||
# * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
# * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
|
||||
# * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
# * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||
# * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
|
||||
# * OF SUCH DAMAGE.
|
||||
# ------------------------------------------------------------------------------
|
||||
function timer_now
|
||||
|
||||
load_conf "prompt"
|
||||
|
||||
# Common helpers, then the ones private to this module (helpers/prompt.sh)
|
||||
# shellcheck source=../helpers/common.sh
|
||||
declare -F _profile_load_helpers >/dev/null || . "$(dirname -- "${BASH_SOURCE[0]}")/../helpers/common.sh"
|
||||
_profile_load_helpers "${BASH_SOURCE[0]}"
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# Parse a prompt theme file safely — it is NEVER sourced or executed.
|
||||
# Two categories of keys are accepted:
|
||||
# PROMPT_COLOR_* — prompt slot colours (TIME_FG, BAR_BG, …)
|
||||
# Standard colour variables from disp.sh (Blue, On_IBlack, …) — allows a
|
||||
# theme to redefine the palette used everywhere in the shell session.
|
||||
# Values follow the forms accepted by _prompt_resolve_color. Any other key or
|
||||
# value is rejected with a warning.
|
||||
# Usage: load_theme <theme_name_or_path> [theme_dir]
|
||||
# theme_name_or_path : bare name (e.g. "dark") or an explicit path.
|
||||
# theme_dir : directory to search for bare names; defaults to
|
||||
# $MYPATH/profile.d/themes. Overridable via
|
||||
# PROMPT_THEME_DIR.
|
||||
load_theme()
|
||||
{
|
||||
date +%s%N
|
||||
local theme_name="$1"
|
||||
local theme_file=""
|
||||
|
||||
[[ -z "$theme_name" ]] && return 0
|
||||
|
||||
theme_file=$(_prompt_theme_file "$theme_name" "${2:-}")
|
||||
|
||||
if [[ ! -f "$theme_file" || ! -r "$theme_file" ]]; then
|
||||
printf "[ Warning ] load_theme: theme file not found: %s\n" "$theme_file" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
# ---- Key whitelist: prompt slots and standard colour variables ----------
|
||||
local -A _lth_allowed=(
|
||||
[PROMPT_COLOR_TIME_FG]=1 [PROMPT_COLOR_TIME_BG]=1
|
||||
[PROMPT_COLOR_BAR_BG]=1
|
||||
[PROMPT_COLOR_OK_FG]=1 [PROMPT_COLOR_OK_MARK]=1
|
||||
[PROMPT_COLOR_ERR_BG]=1 [PROMPT_COLOR_ERR_FG]=1 [PROMPT_COLOR_ERR_MARK]=1
|
||||
[PROMPT_COLOR_ROOT_FG]=1 [PROMPT_COLOR_USER_FG]=1
|
||||
[PROMPT_COLOR_DIR_FG]=1
|
||||
[PROMPT_COLOR_CTX_FG]=1
|
||||
)
|
||||
local _lth_cn
|
||||
for _lth_cn in "${_PROMPT_COLOR_NAMES[@]}"; do
|
||||
_lth_allowed[$_lth_cn]=1
|
||||
done
|
||||
|
||||
# ---- Line parser ---------------------------------------------------------
|
||||
local _lth_line _lth_key _lth_value _lth_lineno=0
|
||||
|
||||
while IFS= read -r _lth_line || [[ -n "$_lth_line" ]]; do
|
||||
_lth_lineno=$(( _lth_lineno + 1 ))
|
||||
_lth_line="${_lth_line%$'\r'}" # strip CR
|
||||
_lth_line="${_lth_line#"${_lth_line%%[![:space:]]*}"}" # ltrim
|
||||
_lth_line="${_lth_line%"${_lth_line##*[![:space:]]}"}" # rtrim
|
||||
[[ -z "$_lth_line" || "$_lth_line" == '#'* ]] && continue # blank/comment
|
||||
[[ "$_lth_line" == 'export '* ]] && _lth_line="${_lth_line#export }" # strip prefix
|
||||
|
||||
if [[ "$_lth_line" != *=* ]]; then
|
||||
printf "[ Warning ] load_theme: %s:%d: not a key=value pair, ignoring.\n" \
|
||||
"$theme_file" "$_lth_lineno" >&2
|
||||
continue
|
||||
fi
|
||||
|
||||
_lth_key="${_lth_line%%=*}"
|
||||
_lth_value="${_lth_line#*=}"
|
||||
_lth_key="${_lth_key#"${_lth_key%%[![:space:]]*}"}"
|
||||
_lth_key="${_lth_key%"${_lth_key##*[![:space:]]}"}" # trim key
|
||||
|
||||
if [[ -z "${_lth_allowed[$_lth_key]+x}" ]]; then
|
||||
printf "[ Warning ] load_theme: %s:%d: key '%s' is not allowed, ignoring.\n" \
|
||||
"$theme_file" "$_lth_lineno" "$_lth_key" >&2
|
||||
continue
|
||||
fi
|
||||
|
||||
# Strip surrounding quotes (handles inline trailing comments like KEY="val" # note)
|
||||
if [[ "$_lth_value" == '"'* ]]; then
|
||||
_lth_value="${_lth_value#\"}"
|
||||
_lth_value="${_lth_value%%\"*}"
|
||||
elif [[ "$_lth_value" == "'"* ]]; then
|
||||
_lth_value="${_lth_value#\'}"
|
||||
_lth_value="${_lth_value%%\'*}"
|
||||
fi
|
||||
|
||||
if _prompt_resolve_color "$_lth_value"; then
|
||||
export "$_lth_key"="$_PROMPT_RESOLVED"
|
||||
else
|
||||
printf "[ Warning ] load_theme: %s:%d: invalid value for '%s', ignoring.\n" \
|
||||
"$theme_file" "$_lth_lineno" "$_lth_key" >&2
|
||||
fi
|
||||
done < "$theme_file"
|
||||
}
|
||||
# Not exported, it remains private (like the other _prompt_* helpers: set_theme
|
||||
# and set_prompt are meant to be used from the interactive shell itself).
|
||||
# ------------------------------------------------------------------------------
|
||||
# Dynamically switch the prompt theme for the current shell session.
|
||||
# Calls load_theme to apply the new colour values immediately, then updates
|
||||
# PROMPT_THEME so subshells and the set_prompt fallback chain reflect the
|
||||
# change. PROMPT_THEME_DIR is honoured when set.
|
||||
# Usage: set_theme [theme_name_or_path]
|
||||
# With no argument (or -l / --list), lists available .theme files.
|
||||
set_theme()
|
||||
{
|
||||
local theme_dir="${PROMPT_THEME_DIR:-${MYPATH}/profile.d/themes}"
|
||||
local preview=0
|
||||
local save=0
|
||||
local list_only=0
|
||||
local theme_name=""
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
-h|--help)
|
||||
printf "set_theme: Switch the prompt colour theme for the current shell session.\n\n"
|
||||
printf "Usage: set_theme [options] [theme]\n\n"
|
||||
printf "Options:\n"
|
||||
printf "\t-h, --help\tDisplay this help screen\n"
|
||||
printf "\t-l, --list\tList available themes (default when no argument is given)\n"
|
||||
printf "\t-p, --preview\tPreview a theme without applying it\n"
|
||||
printf "\t-S, --save\tSave theme to configuration\n\n"
|
||||
printf "Arguments:\n"
|
||||
printf "\ttheme \tBare theme name (e.g. 'dark') or an explicit path to a .theme file.\n"
|
||||
printf "\t \tThemes are searched in: %s\n" "$theme_dir"
|
||||
printf "\t \tOverride with PROMPT_THEME_DIR in profile.conf [prompt].\n\n"
|
||||
printf "Examples:\n"
|
||||
printf "\tset_theme \t— list available themes\n"
|
||||
printf "\tset_theme dark \t— apply the dark theme\n"
|
||||
printf "\tset_theme -p dark \t— preview the dark theme\n"
|
||||
printf "\tset_theme -S \t— save the current theme to the configuration\n"
|
||||
printf "\tset_theme -S dark \t— apply and save the dark theme\n"
|
||||
printf "\tset_theme ~/my.theme\t— apply a theme by path\n"
|
||||
return 0
|
||||
;;
|
||||
-l|--list)
|
||||
list_only=1
|
||||
shift
|
||||
;;
|
||||
-p|--preview)
|
||||
preview=1
|
||||
shift
|
||||
;;
|
||||
-S|--save)
|
||||
save=1
|
||||
shift
|
||||
;;
|
||||
--)
|
||||
shift
|
||||
break
|
||||
;;
|
||||
-*)
|
||||
disp E "Unknown option: $1, use \"set_theme --help\" to display usage."
|
||||
return 1
|
||||
;;
|
||||
*)
|
||||
if [[ -n "$theme_name" ]]; then
|
||||
disp E "Too many arguments. Usage: set_theme [options] [theme]"
|
||||
return 1
|
||||
fi
|
||||
theme_name="$1"
|
||||
shift
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ $# -gt 0 ]]; then
|
||||
if [[ -n "$theme_name" ]]; then
|
||||
disp E "Too many arguments. Usage: set_theme [options] [theme]"
|
||||
return 1
|
||||
fi
|
||||
theme_name="$1"
|
||||
shift
|
||||
fi
|
||||
|
||||
# -- list mode -----------------------------------------------------------
|
||||
if (( list_only )) || [[ -z "$theme_name" && $preview -eq 0 ]]; then
|
||||
if (( save )); then
|
||||
if [[ -n "${PROMPT_THEME:-}" ]]; then
|
||||
conf_save "prompt" "PROMPT_THEME" "$PROMPT_THEME" || return 1
|
||||
disp I "Saved current prompt theme '$PROMPT_THEME' to configuration."
|
||||
return 0
|
||||
fi
|
||||
disp E "No active theme to save. Apply a theme first or pass one with -S."
|
||||
return 1
|
||||
fi
|
||||
|
||||
printf "Available themes in %s:\n" "$theme_dir"
|
||||
local f name
|
||||
for f in "$theme_dir"/*.theme; do
|
||||
[[ -f "$f" ]] || continue
|
||||
name="${f##*/}"
|
||||
name="${name%.theme}"
|
||||
if [[ "$name" == "${PROMPT_THEME:-}" ]]; then
|
||||
printf " * %s (active)\n" "$name"
|
||||
else
|
||||
printf " %s\n" "$name"
|
||||
fi
|
||||
done
|
||||
return 0
|
||||
fi
|
||||
|
||||
if (( preview )) && [[ -z "$theme_name" ]]; then
|
||||
disp E "--preview requires a theme argument."
|
||||
return 1
|
||||
fi
|
||||
|
||||
if (( preview && save )); then
|
||||
disp E "--preview and --save cannot be used together."
|
||||
return 1
|
||||
fi
|
||||
|
||||
# -- preview mode --------------------------------------------------------
|
||||
if (( preview )); then
|
||||
# Snapshot the palette and the prompt colours by value, to put back
|
||||
# exactly what was there (including the PROMPT_COLOR_* that did not
|
||||
# exist, which a previewed theme would otherwise leave behind).
|
||||
local -A _snap=()
|
||||
local _v
|
||||
for _v in "${_PROMPT_COLOR_NAMES[@]}" ${!PROMPT_COLOR_@}; do
|
||||
[[ -n "${!_v+x}" ]] && _snap[$_v]="${!_v}"
|
||||
done
|
||||
|
||||
local _rc=0
|
||||
_prompt_load_theme_clean "$theme_name" "$theme_dir" || _rc=1
|
||||
|
||||
if (( _rc == 0 )); then
|
||||
printf "Preview for theme '%s':\n" "$theme_name"
|
||||
printf "%b [ 13:37:00 ] %b%b [ 0 %b• (5s) %b] %b user@host%b [ git:main +1/-0 ] %b\n" \
|
||||
"${PROMPT_COLOR_TIME_FG:-}${PROMPT_COLOR_TIME_BG:-}" "${DEFAULTCOL:-}" \
|
||||
"${PROMPT_COLOR_OK_FG:-}${PROMPT_COLOR_BAR_BG:-}" \
|
||||
"${PROMPT_COLOR_OK_MARK:-}${PROMPT_COLOR_BAR_BG:-}" \
|
||||
"${PROMPT_COLOR_OK_FG:-}${PROMPT_COLOR_BAR_BG:-}" \
|
||||
"${PROMPT_COLOR_USER_FG:-}${PROMPT_COLOR_BAR_BG:-}" \
|
||||
"${PROMPT_COLOR_CTX_FG:-}${PROMPT_COLOR_BAR_BG:-}" "${DEFAULTCOL:-}"
|
||||
printf "%b [ 13:37:00 ] %b%b [ 0 %b• (5s) %b] %b user@host%b [ git:main +1/-0 ] %b\n" \
|
||||
"${PROMPT_COLOR_TIME_FG:-}${PROMPT_COLOR_TIME_BG:-}" "${DEFAULTCOL:-}" \
|
||||
"${PROMPT_COLOR_OK_FG:-}${PROMPT_COLOR_ERR_BG:-}" \
|
||||
"${PROMPT_COLOR_ERR_MARK:-}${PROMPT_COLOR_ERR_BG:-}" \
|
||||
"${PROMPT_COLOR_OK_FG:-}${PROMPT_COLOR_ERR_BG:-}" \
|
||||
"${PROMPT_COLOR_USER_FG:-}${PROMPT_COLOR_BAR_BG:-}" \
|
||||
"${PROMPT_COLOR_CTX_FG:-}${PROMPT_COLOR_BAR_BG:-}" "${DEFAULTCOL:-}"
|
||||
printf "%b/path/to/dir \$ %b\n" \
|
||||
"${PROMPT_COLOR_DIR_FG:-}" "${DEFAULTCOL:-}"
|
||||
fi
|
||||
|
||||
# Restore: every colour variable is either put back or removed
|
||||
for _v in "${_PROMPT_COLOR_NAMES[@]}" ${!PROMPT_COLOR_@}; do
|
||||
if [[ -n "${_snap[$_v]+x}" ]]; then
|
||||
export "$_v=${_snap[$_v]}"
|
||||
else
|
||||
unset "$_v"
|
||||
fi
|
||||
done
|
||||
return "$_rc"
|
||||
fi
|
||||
|
||||
# -- apply mode ----------------------------------------------------------
|
||||
|
||||
_prompt_load_theme_clean "$theme_name" "$theme_dir" || return 1
|
||||
|
||||
export PROMPT_THEME="$theme_name"
|
||||
if (( save )); then
|
||||
conf_save "prompt" "PROMPT_THEME" "$theme_name" || return 1
|
||||
disp I "Prompt theme set to $theme_name and saved to configuration."
|
||||
return 0
|
||||
fi
|
||||
|
||||
disp I "Prompt theme set to $theme_name."
|
||||
}
|
||||
export -f set_theme
|
||||
# ------------------------------------------------------------------------------
|
||||
# timer_* functions : internal timing functions for the prompt.
|
||||
# The start is recorded by the DEBUG trap (profile.sh) before each command and
|
||||
# the elapsed time is computed by set_prompt, through PROMPT_COMMAND.
|
||||
|
||||
# Usage: timer_start
|
||||
# Record the start time of the command about to run, unless already recorded
|
||||
# (the DEBUG trap fires for every simple command of a pipeline or function: only
|
||||
# the first one counts).
|
||||
timer_start()
|
||||
{
|
||||
if [[ -z "${_timer_start_us:-}" ]]; then
|
||||
_timer_now
|
||||
# shellcheck disable=SC2154 # set by _timer_now (helpers/prompt.sh)
|
||||
_timer_start_us="$_timer_us"
|
||||
fi
|
||||
}
|
||||
|
||||
function timer_start
|
||||
# Usage: timer_stop
|
||||
# Compute the time elapsed since timer_start and format it into timer_show with
|
||||
# an appropriate unit (us, ms, s, m, h).
|
||||
timer_stop()
|
||||
{
|
||||
timer_start=${timer_start:-$(timer_now)}
|
||||
}
|
||||
|
||||
function timer_stop
|
||||
{
|
||||
local delta_us=$((($(timer_now) - $timer_start) / 1000))
|
||||
_timer_now
|
||||
local delta_us=$(( _timer_us - ${_timer_start_us:-$_timer_us} ))
|
||||
(( delta_us < 0 )) && delta_us=0
|
||||
local us=$((delta_us % 1000))
|
||||
local ms=$(((delta_us / 1000) % 1000))
|
||||
local s=$(((delta_us / 1000000) % 60))
|
||||
@@ -27,7 +343,7 @@ function timer_stop
|
||||
elif ((s >= 10)); then
|
||||
timer_show=${s}.$((ms / 100))s
|
||||
elif ((s > 0)); then
|
||||
timer_show=${s}.$(printf %03d $ms)s
|
||||
printf -v timer_show '%d.%03ds' "$s" "$ms"
|
||||
elif ((ms >= 100)); then
|
||||
timer_show=${ms}ms
|
||||
elif ((ms > 0)); then
|
||||
@@ -35,56 +351,115 @@ function timer_stop
|
||||
else
|
||||
timer_show=${us}us
|
||||
fi
|
||||
unset timer_start
|
||||
unset _timer_start_us
|
||||
}
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# Function triguered internaly by bash : defining prompt
|
||||
# Prompt segments. Each one puts its text in _PROMPT_SEG (empty when there is
|
||||
# nothing to show), without a command substitution (no subshell per segment).
|
||||
# They are defined once, at load time, and not inside set_prompt: a function
|
||||
# definition resets $?, and set_prompt must read the exit status of the user's
|
||||
# last command before anything else runs.
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
set_prompt ()
|
||||
# Function triggered internally by bash : defining prompt
|
||||
# Usage: set_prompt
|
||||
# This function is called by bash before displaying the prompt. It sets the
|
||||
# PS1 variable to a custom prompt that includes the exit status of the last
|
||||
# command, the elapsed time of the last command, and the current user and host.
|
||||
set_prompt()
|
||||
{
|
||||
Last_Command=$? # Must come first!
|
||||
Blue='\[\e[0;34m\]'
|
||||
White='\[\e[01;37m\]'
|
||||
Yellow='\[\e[01;93m\]'
|
||||
Red='\[\e[01;31m\]'
|
||||
Green='\[\e[01;32m\]'
|
||||
OnGrey='\[\e[47m\]'
|
||||
OnRed='\[\e[41m\]'
|
||||
OnBlue='\[\e[44m\]'
|
||||
ICyan='\[\e[0;96m\]'
|
||||
Default='\[\e[00m\]'
|
||||
FancyX='\342\234\227'
|
||||
Checkmark='\342\234\223'
|
||||
|
||||
# Begin with time
|
||||
PS1="\[\e[s$Blue$OnGrey [ \t ] $OnBlue"
|
||||
|
||||
# Add a bright white exit status for the last command
|
||||
|
||||
# If it was successful, print a green check mark. Otherwise, print
|
||||
# a red X.
|
||||
if [[ $Last_Command == 0 ]]; then
|
||||
PS1+="$White$OnBlue [ \$Last_Command "
|
||||
PS1+="$Green$Checkmark "
|
||||
else
|
||||
PS1+="$White$OnRed [ \$Last_Command "
|
||||
PS1+="$Yellow$FancyX "
|
||||
local Last_Command=$? # Must come first: nothing may run before this line
|
||||
# When other commands precede set_prompt in PROMPT_COMMAND (profile.sh adds
|
||||
# _profile_save_status first for that case), $? is theirs: use the saved one.
|
||||
if [[ -n "${_profile_status+x}" ]]; then
|
||||
Last_Command="$_profile_status"
|
||||
unset _profile_status
|
||||
fi
|
||||
local FancyX='\342\234\227'
|
||||
local Checkmark='\342\234\223'
|
||||
|
||||
# Add the ellapsed time and current date
|
||||
# The context segment is not copied into PS1. Bash expands PS1 when
|
||||
# displaying it, including $(...) and `...`: a git branch (or conda
|
||||
# environment) named 'x$(command)' would be executed. PS1 only contains the
|
||||
# reference ${_PROMPT_CTX}, whose value is inserted as plain text, never
|
||||
# re-scanned. This needs promptvars (the default).
|
||||
shopt -q promptvars || shopt -s promptvars
|
||||
|
||||
# Resolve theme/config colours with hardcoded fallbacks
|
||||
local _time_fg="${PROMPT_COLOR_TIME_FG:-$Blue}"
|
||||
local _time_bg="${PROMPT_COLOR_TIME_BG:-$On_White}"
|
||||
local _bar_bg="${PROMPT_COLOR_BAR_BG:-$On_Blue}"
|
||||
local _ok_fg="${PROMPT_COLOR_OK_FG:-$BWhite}"
|
||||
local _ok_mark="${PROMPT_COLOR_OK_MARK:-$BGreen}"
|
||||
local _err_bg="${PROMPT_COLOR_ERR_BG:-$On_Red}"
|
||||
local _err_fg="${PROMPT_COLOR_ERR_FG:-$White}"
|
||||
local _err_mark="${PROMPT_COLOR_ERR_MARK:-$BYellow}"
|
||||
local _root_fg="${PROMPT_COLOR_ROOT_FG:-$Red}"
|
||||
local _user_fg="${PROMPT_COLOR_USER_FG:-$BGreen}"
|
||||
local _dir_fg="${PROMPT_COLOR_DIR_FG:-$ICyan}"
|
||||
local _ctx_fg="${PROMPT_COLOR_CTX_FG:-$BIYellow}"
|
||||
|
||||
# Begin with time (cursor-save is non-printing; all ANSI sequences wrapped
|
||||
# in \[...\] so bash does not count them toward the visible line width).
|
||||
# Every fg colour is combined with its section bg in the same \[...\] block
|
||||
# so that even "reset" colours (0;Xm) cannot strip the background.
|
||||
PS1="\[\e[s\]\[${_time_fg}${_time_bg}\] [ \t ] \[${_bar_bg}\]"
|
||||
|
||||
# Add exit status of the last command.
|
||||
# If it was successful, print a green check mark. Otherwise, print a red X.
|
||||
timer_stop
|
||||
PS1+="($timer_show)$White ] $OnBlue "
|
||||
|
||||
# If root, just print the host in red. Otherwise, print the current user
|
||||
# and host in green.
|
||||
if [[ $EUID -eq 0 ]]; then
|
||||
PS1+="$Red\\u$Green@\\h"
|
||||
if [[ $Last_Command == 0 ]]; then
|
||||
PS1+="\[${_ok_fg}${_bar_bg}\] [ $Last_Command "
|
||||
PS1+="\[${_ok_mark}${_bar_bg}\]${Checkmark} "
|
||||
# Add the elapsed time, then close the status section and return to bar bg.
|
||||
PS1+="($timer_show)\[${_ok_fg}${_bar_bg}\] ] "
|
||||
else
|
||||
PS1+="$Green\\u@\\h"
|
||||
PS1+="\[${_err_fg}${_err_bg}\] [ $Last_Command "
|
||||
PS1+="\[${_err_mark}${_err_bg}\]${FancyX} "
|
||||
PS1+="($timer_show)\[${_err_fg}${_err_bg}\] ] "
|
||||
fi
|
||||
PS1+="\e[K\e[u$Default\n"
|
||||
# Print the working directory and prompt marker in blue, and reset
|
||||
# the text color to the default.
|
||||
PS1+="$ICyan\\w \\\$$Default "
|
||||
|
||||
# If root, print the host in root colour. Otherwise use user colour.
|
||||
if [[ $EUID -eq 0 ]]; then
|
||||
PS1+="\[${_root_fg}${_bar_bg}\] \\u\[${_user_fg}${_bar_bg}\]@\\h"
|
||||
else
|
||||
PS1+="\[${_user_fg}${_bar_bg}\] \\u@\\h"
|
||||
fi
|
||||
|
||||
# Optional context segment appended at the end of the top bar.
|
||||
_PROMPT_CTX=""
|
||||
_prompt_seg_git; _prompt_ctx_add
|
||||
_prompt_seg_conda; _prompt_ctx_add
|
||||
_prompt_seg_venv; _prompt_ctx_add
|
||||
_prompt_seg_session; _prompt_ctx_add
|
||||
if [[ -n "$_PROMPT_CTX" ]]; then
|
||||
# Single quotes: the reference must reach PS1 literally (see above)
|
||||
PS1+="\[${_ctx_fg}${_bar_bg}\] [ "'${_PROMPT_CTX}'" ]"
|
||||
fi
|
||||
|
||||
PS1+="\[\e[K\e[u\]\[$RESETCOL\]\n"
|
||||
# Print the working directory and prompt marker, then reset colour.
|
||||
PS1+="\[${_dir_fg}\]\\w \\\$\[$RESETCOL\] "
|
||||
}
|
||||
# ------------------------------------------------------------------------------
|
||||
# Theme and configuration loading.
|
||||
# Precedence (lowest → highest):
|
||||
# 1. Hardcoded fallbacks in set_prompt
|
||||
# 2. Theme file (PROMPT_THEME key from [prompt] section)
|
||||
# 3. Individual PROMPT_COLOR_* overrides in [prompt] section
|
||||
#
|
||||
# load_conf "prompt" (top of this file) has exported every key of the section,
|
||||
# the theme file is loaded over them, and _prompt_apply_overrides then puts the
|
||||
# PROMPT_COLOR_* keys of the section back on top (resolving $ColorName values).
|
||||
# CONF_prompt is already populated by parse_conf (run in profile.sh before
|
||||
# modules are sourced), which is where PROMPT_THEME and PROMPT_THEME_DIR are
|
||||
# read from.
|
||||
_pt_theme="${CONF_prompt[PROMPT_THEME]:-}"
|
||||
_pt_dir="${CONF_prompt[PROMPT_THEME_DIR]:-}"
|
||||
[[ -n "$_pt_theme" ]] && load_theme "$_pt_theme" ${_pt_dir:+"$_pt_dir"}
|
||||
unset _pt_theme _pt_dir
|
||||
_prompt_apply_overrides
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
# EOF
|
||||
|
||||
Executable → Regular
+495
-136
@@ -1,151 +1,510 @@
|
||||
#!/usr/bin/env bash
|
||||
# ------------------------------------------------------------------------------
|
||||
# Copyright (c) 2013-2026 Geoffray Levasseur <fatalerrors@geoffray-levasseur.org>
|
||||
# Protected by the BSD3 license. Please read bellow for details.
|
||||
#
|
||||
# * Redistribution and use in source and binary forms,
|
||||
# * with or without modification, are permitted provided
|
||||
# * that the following conditions are met:
|
||||
# *
|
||||
# * Redistributions of source code must retain the above
|
||||
# * copyright notice, this list of conditions and the
|
||||
# * following disclaimer.
|
||||
# *
|
||||
# * Redistributions in binary form must reproduce the above
|
||||
# * copyright notice, this list of conditions and the following
|
||||
# * disclaimer in the documentation and/or other materials
|
||||
# * provided with the distribution.
|
||||
# *
|
||||
# * Neither the name of the copyright holder nor the names
|
||||
# * of any other contributors may be used to endorse or
|
||||
# * promote products derived from this software without
|
||||
# * specific prior written permission.
|
||||
# *
|
||||
# * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
|
||||
# * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
|
||||
# * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
|
||||
# * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
# * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
|
||||
# * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
# * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
|
||||
# * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
# * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||
# * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
|
||||
# * OF SUCH DAMAGE.
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
load_conf "pwd"
|
||||
|
||||
# Common helpers, then the ones private to this module (helpers/pwd.sh)
|
||||
# shellcheck source=../helpers/common.sh
|
||||
declare -F _profile_load_helpers >/dev/null || . "$(dirname -- "${BASH_SOURCE[0]}")/../helpers/common.sh"
|
||||
_profile_load_helpers "${BASH_SOURCE[0]}"
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# genpwd : generate a password with different criteria
|
||||
# default 16 car with up and low car, symbol and number
|
||||
# Usage: genpwd [options] [--extracars=<cars>] [--length=<n>] [nb_passwd]
|
||||
# Options:
|
||||
# -h, --help Display that help screen
|
||||
# -s, --nosymbols Exclude symbols
|
||||
# -n, --nonumbers Exclude numbers
|
||||
# -u, --noup Exclude uppercase letters
|
||||
# -l, --nolow Exclude lowercase letters
|
||||
# -e=<c>, --extracars=<c>
|
||||
# Add the given caracters to the possible caracter list
|
||||
# -L=<n>, --length=<n>
|
||||
# Set length of the password (default is 16)
|
||||
# -o=<n>, --occurences=<n>
|
||||
# Set the maximum occurences of a same caracter (default is 2)
|
||||
# The function is very slow on Windows
|
||||
# ------------------------------------------------------------------------------
|
||||
genpwd()
|
||||
{
|
||||
local length=16
|
||||
local occurs=2 # Bug, if set to 1, seems to be ignored
|
||||
local symb=1 maj=1 min=1 numb=1
|
||||
local nbpwd=1
|
||||
|
||||
for opt in $@; do
|
||||
case $opt in
|
||||
"-h"|"--help")
|
||||
echo "genpwd: generate a secure random password."
|
||||
echo
|
||||
echo "Usage: genpwd [options] [--extracars=<cars>] [--length=<n>] [nb_passwd]"
|
||||
echo
|
||||
echo "Options:"
|
||||
echo " -h, --help Display that help screen"
|
||||
echo " -s, --nosymbols Exclude symbols"
|
||||
echo " -n, --nonumbers Exclude numbers"
|
||||
echo " -u, --noup Exclude uppercase letters"
|
||||
echo " -l, --nolow Exclude lowercase letters"
|
||||
echo " -e=<c>, --extracars=<c>"
|
||||
echo " Add the given caracters to the possible caracter list"
|
||||
echo " -L=<n>, --length=<n>"
|
||||
echo " Set length of the password (default is $length)"
|
||||
echo " -o=<n>, --occurences=<n>"
|
||||
echo " Set the maximum occurences of a same caracter (default is $occurs)"
|
||||
echo
|
||||
echo "If the --extracars parameter is given, at least one of the given caracter will"
|
||||
echo "be used in the final password."
|
||||
echo
|
||||
echo "Please note that some caracters might be interpreted by Bash or Awk programs,"
|
||||
echo "and thus, cannot be used without provoquing errors. Those identified caracters"
|
||||
echo "are :"
|
||||
echo ' * ? \ $ { }'
|
||||
echo
|
||||
return 0
|
||||
;;
|
||||
"-s"|"--nosymbols")
|
||||
symb=0
|
||||
;;
|
||||
"-n"|"--nonumbers")
|
||||
numb=0
|
||||
;;
|
||||
"-u"|"--noup")
|
||||
maj=0
|
||||
;;
|
||||
"-l"|"--nolow")
|
||||
min=0
|
||||
;;
|
||||
"-e"?*|"--extracars"?*)
|
||||
local extcar=$(echo "$opt" | cut -f 2- -d '=')
|
||||
;;
|
||||
"-L"?*|"--length"?*)
|
||||
local length=$(echo "$opt" | cut -f 2- -d '=')
|
||||
if ! [[ $length =~ ^[0-9]+$ ]]; then
|
||||
echo "The --length parameter requires a number."
|
||||
return 1
|
||||
fi
|
||||
;;
|
||||
"-o"?*|"--occurences"?*)
|
||||
local occurs=$(echo "$opt" | cut -f 2- -d '=')
|
||||
if ! [[ $occurs =~ ^[1-9]+$ ]]; then
|
||||
echo "The --occurs parameter requires a number from 1 to 9."
|
||||
return 1
|
||||
fi
|
||||
;;
|
||||
"-*")
|
||||
echo "Unknow parameter ${opt}."
|
||||
return 1
|
||||
;;
|
||||
*)
|
||||
if ! [[ $opt =~ ^[1-9]+$ ]]; then
|
||||
echo "Unknow parameter ${opt}."
|
||||
return 1
|
||||
else
|
||||
local nbpwd=$opt
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
local length=${GENPWD_DEFAULT_LENGTH:-16}
|
||||
local occurs=${GENPWD_DEFAULT_OCCURS:-2}
|
||||
local symb=${GENPWD_DEFAULT_SYMBOLS:-1}
|
||||
local maj=${GENPWD_DEFAULT_UPPERCASE:-1}
|
||||
local min=${GENPWD_DEFAULT_LOWERCASE:-1}
|
||||
local numb=${GENPWD_DEFAULT_NUMBERS:-1}
|
||||
local nbpwd=${GENPWD_DEFAULT_COUNT:-1}
|
||||
local extcar=""
|
||||
|
||||
local PARSED
|
||||
PARSED=$(getopt -o hsnule:L:o: --long \
|
||||
help,nosymbols,nonumbers,noup,nolow,extracars:,length:,occurences:,occurrences: \
|
||||
-n 'genpwd' -- "$@")
|
||||
# shellcheck disable=SC2181 # getopt return code is checked immediately after
|
||||
if [[ $? -ne 0 ]]; then
|
||||
disp E "Invalid options, use \"genpwd --help\" to display usage."
|
||||
return 1
|
||||
fi
|
||||
# Restore the getopt-normalised (shell-quoted) argument list
|
||||
local -a _pargs=()
|
||||
_getopt_split _pargs "$PARSED"
|
||||
set -- "${_pargs[@]}"
|
||||
|
||||
while true; do
|
||||
case "$1" in
|
||||
-h|--help)
|
||||
printf "genpwd: Generate random password(s).\n\n"
|
||||
printf "Usage: genpwd [options] [nb_passwd]\n\n"
|
||||
printf "Options:\n"
|
||||
printf "\t-h, --help\t\tDisplay this help screen\n"
|
||||
printf "\t-s, --nosymbols\t\tExclude symbols\n"
|
||||
printf "\t-n, --nonumbers\t\tExclude numbers\n"
|
||||
printf "\t-u, --noup\t\tExclude uppercase letters\n"
|
||||
printf "\t-l, --nolow\t\tExclude lowercase letters\n"
|
||||
printf "\t-e, --extracars <c>\tAdd characters to the pool\n"
|
||||
printf "\t-L, --length <n>\tSet password length (default: 16)\n"
|
||||
printf "\t-o, --occurences <n>\tMax occurrences per character (default: 2)\n"
|
||||
return 0
|
||||
;;
|
||||
-s|--nosymbols)
|
||||
symb=0
|
||||
shift
|
||||
;;
|
||||
-n|--nonumbers)
|
||||
numb=0
|
||||
shift
|
||||
;;
|
||||
-u|--noup)
|
||||
maj=0
|
||||
shift
|
||||
;;
|
||||
-l|--nolow)
|
||||
min=0
|
||||
shift
|
||||
;;
|
||||
-e|--extracars)
|
||||
extcar="$2"
|
||||
shift 2
|
||||
;;
|
||||
-L|--length)
|
||||
length="$2"
|
||||
if ! [[ $length =~ ^[1-9][0-9]*$ ]]; then
|
||||
disp E "The --length parameter requires a positive integer."
|
||||
return 1
|
||||
fi
|
||||
shift 2
|
||||
;;
|
||||
-o|--occurences|--occurrences)
|
||||
occurs="$2"
|
||||
if ! [[ $occurs =~ ^[1-9][0-9]*$ ]]; then
|
||||
disp E "The --occurences parameter requires a positive integer."
|
||||
return 1
|
||||
fi
|
||||
shift 2
|
||||
;;
|
||||
--)
|
||||
shift
|
||||
break
|
||||
;;
|
||||
*)
|
||||
disp E "Invalid options, use \"genpwd --help\" to display usage."
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
# Function selecting a random caracter from the list in parameter
|
||||
pickcar()
|
||||
{
|
||||
# When a character is picked we check if it's not appearing already twice
|
||||
# elsewhere, we choose an other char, to compensate weak bash randomizer
|
||||
while [[ -z $char ]]; do
|
||||
local char=$(echo ${1:RANDOM%${#1}:1} $RANDOM)
|
||||
if [[ $(awk -F"$char" '{print NF-1}' <<< "$picked") -gt $occurs ]]; then
|
||||
unset char
|
||||
fi
|
||||
done
|
||||
picked+="$char"
|
||||
echo "$char"
|
||||
if [[ $# -gt 1 ]]; then
|
||||
disp E "Too many positional arguments. Use only [nb_passwd]."
|
||||
return 1
|
||||
fi
|
||||
|
||||
if [[ $# -eq 1 ]]; then
|
||||
nbpwd="$1"
|
||||
if ! [[ $nbpwd =~ ^[1-9][0-9]*$ ]]; then
|
||||
disp E "The number of passwords to generate must be a positive integer."
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
|
||||
local carset=""
|
||||
# The defaults may come from the configuration file: bash evaluates a
|
||||
# non-numeric string inside (( )) as an expression, so check them first.
|
||||
local _v
|
||||
for _v in "$symb" "$numb" "$maj" "$min"; do
|
||||
if [[ $_v != 0 && $_v != 1 ]]; then
|
||||
disp E "Invalid GENPWD_DEFAULT_* value '$_v': the character class switches must be 0 or 1."
|
||||
return 1
|
||||
fi
|
||||
done
|
||||
for _v in "$length" "$occurs" "$nbpwd"; do
|
||||
if ! [[ $_v =~ ^[1-9][0-9]*$ ]]; then
|
||||
disp E "Invalid GENPWD_DEFAULT_* value '$_v': length, occurrences and count must be positive integers."
|
||||
return 1
|
||||
fi
|
||||
done
|
||||
local unique_carset=""
|
||||
local ch=""
|
||||
local i=0
|
||||
local n=0
|
||||
local idx=0
|
||||
local attempts=0
|
||||
local count=0
|
||||
local max_attempts=0
|
||||
local set=""
|
||||
local char=""
|
||||
local -a required_sets=()
|
||||
declare -A seen_chars=()
|
||||
|
||||
(( symb )) && {
|
||||
required_sets+=('!.@#&%/^-_')
|
||||
carset+='!.@#&%/^-_'
|
||||
}
|
||||
(( numb )) && {
|
||||
required_sets+=('0123456789')
|
||||
carset+='0123456789'
|
||||
}
|
||||
(( maj )) && {
|
||||
required_sets+=('ABCDEFGHIJKLMNOPQRSTUVWXYZ')
|
||||
carset+='ABCDEFGHIJKLMNOPQRSTUVWXYZ'
|
||||
}
|
||||
(( min )) && {
|
||||
required_sets+=('abcdefghijklmnopqrstuvwxyz')
|
||||
carset+='abcdefghijklmnopqrstuvwxyz'
|
||||
}
|
||||
if [[ -n $extcar ]]; then
|
||||
required_sets+=("$extcar")
|
||||
carset+="$extcar"
|
||||
fi
|
||||
|
||||
echo "Generating $nbpwd passwords, please wait..."
|
||||
for n in $( seq 1 $nbpwd ); do
|
||||
{
|
||||
local carset='' # store final caracter set to use
|
||||
local picked='' # store already used caracter
|
||||
local rlength=0 # store already assigned length of caracters
|
||||
if [[ -z $carset ]]; then
|
||||
disp E "No characters are available. Re-enable at least one character class."
|
||||
return 1
|
||||
fi
|
||||
|
||||
# ?, *, $ and \ impossible to use to my knowledge as it would be interpreted
|
||||
if [[ $symb == 1 ]]; then
|
||||
pickcar '!.@#&%/^-_'
|
||||
carset+='!.@#&%/^-_'
|
||||
(( rlength++ ))
|
||||
fi
|
||||
if [[ $numb == 1 ]]; then
|
||||
pickcar '0123456789'
|
||||
carset+='0123456789'
|
||||
(( rlength++ ))
|
||||
fi
|
||||
if [[ $maj == 1 ]]; then
|
||||
pickcar 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
|
||||
carset+='ABCDEFGHIJKLMNOPQRSTUVWXYZ'
|
||||
(( rlength++ ))
|
||||
fi
|
||||
if [[ $min == 1 ]]; then
|
||||
pickcar 'abcdefghijklmnopqrstuvwxyz'
|
||||
carset+='abcdefghijklmnopqrstuvwxyz'
|
||||
(( rlength++ ))
|
||||
fi
|
||||
if [[ -n $extcar ]]; then
|
||||
pickcar "$extcar"
|
||||
carset+=$extcar
|
||||
(( rlength++ ))
|
||||
fi
|
||||
for (( i=0; i<${#carset}; i++ )); do
|
||||
ch=${carset:i:1}
|
||||
if [[ -z ${seen_chars["$ch"]+x} ]]; then
|
||||
seen_chars["$ch"]=1
|
||||
unique_carset+="$ch"
|
||||
fi
|
||||
done
|
||||
unset seen_chars
|
||||
carset="$unique_carset"
|
||||
|
||||
# Check if we have enough car to have something viable
|
||||
if [[ ${#carset} -lt $length ]]; then
|
||||
echo 'Error: not enought caracters are authorised for the password length.'
|
||||
echo 'Please allow more caracter (preferably) or reduce password lentgh.'
|
||||
return 1
|
||||
fi
|
||||
if (( ${#required_sets[@]} > length )); then
|
||||
disp E "The selected character classes require a longer password."
|
||||
return 1
|
||||
fi
|
||||
|
||||
for i in $( seq 1 $(( $length - $rlength )) ); do
|
||||
pickcar "$carset"
|
||||
done
|
||||
} | sort -R | awk '{printf "%s", $1}'
|
||||
unset picked carset rlength
|
||||
echo
|
||||
if (( length > ${#carset} * occurs )); then
|
||||
disp E "The occurrence limit is too strict for the selected length."
|
||||
disp E "Please allow more characters or increase --occurences."
|
||||
return 1
|
||||
fi
|
||||
|
||||
# On stderr: stdout carries the passwords only, so that "pw=$(genpwd)" or
|
||||
# "genpwd | xclip" do not capture this message.
|
||||
disp I "Generating $nbpwd password(s), please wait..." >&2
|
||||
for (( n=1; n<=nbpwd; n++ )); do
|
||||
local -a password_chars=()
|
||||
local -A char_count=()
|
||||
max_attempts=$(( ${#carset} * (occurs + 1) + 32 ))
|
||||
|
||||
for set in "${required_sets[@]}"; do
|
||||
attempts=0
|
||||
while :; do
|
||||
if (( attempts >= max_attempts )); then
|
||||
disp E "Unable to satisfy the occurrence limit with the current settings."
|
||||
return 1
|
||||
fi
|
||||
|
||||
_pwd_rand "${#set}" || return 1
|
||||
idx=$_PWD_RAND
|
||||
char=${set:idx:1}
|
||||
count=${char_count["$char"]:-0}
|
||||
|
||||
if (( count < occurs )); then
|
||||
char_count["$char"]=$(( count + 1 ))
|
||||
password_chars+=("$char")
|
||||
break
|
||||
fi
|
||||
|
||||
attempts=$(( attempts + 1 ))
|
||||
done
|
||||
done
|
||||
|
||||
while (( ${#password_chars[@]} < length )); do
|
||||
attempts=0
|
||||
while :; do
|
||||
if (( attempts >= max_attempts )); then
|
||||
disp E "Unable to satisfy the occurrence limit with the current settings."
|
||||
return 1
|
||||
fi
|
||||
|
||||
_pwd_rand "${#carset}" || return 1
|
||||
idx=$_PWD_RAND
|
||||
char=${carset:idx:1}
|
||||
count=${char_count["$char"]:-0}
|
||||
|
||||
if (( count < occurs )); then
|
||||
char_count["$char"]=$(( count + 1 ))
|
||||
password_chars+=("$char")
|
||||
break
|
||||
fi
|
||||
|
||||
attempts=$(( attempts + 1 ))
|
||||
done
|
||||
done
|
||||
|
||||
for (( i=${#password_chars[@]} - 1; i>0; i-- )); do
|
||||
_pwd_rand "$(( i + 1 ))" || return 1
|
||||
idx=$_PWD_RAND
|
||||
char=${password_chars[i]}
|
||||
password_chars[i]=${password_chars[idx]}
|
||||
password_chars[idx]=$char
|
||||
done
|
||||
|
||||
printf '%s' "${password_chars[@]}"
|
||||
printf '\n'
|
||||
done
|
||||
}
|
||||
export -f genpwd
|
||||
# ------------------------------------------------------------------------------
|
||||
# pwdscore : score a password quality from 1 to 100
|
||||
# Usage: pwdscore [options] <password>
|
||||
pwdscore()
|
||||
{
|
||||
local verbose=${PWDSCORE_DEFAULT_VERBOSE:-0}
|
||||
local read_stdin=0
|
||||
local password=""
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
-h|--help)
|
||||
printf "pwdscore: Score a password from 1 to 100.\n\n"
|
||||
printf "Usage: pwdscore [options] <password>\n"
|
||||
printf " pwdscore [options] --stdin\n"
|
||||
printf " pwdscore [options] # prompt on terminal\n\n"
|
||||
printf "Options:\n"
|
||||
printf "\t-h, --help\t\tDisplay this help screen\n"
|
||||
printf "\t-v, --verbose\t\tShow details about the computed score\n"
|
||||
printf "\t-i, --stdin\t\tRead the password from standard input\n\n"
|
||||
printf "Note:\n"
|
||||
printf " Passwords containing '!' should be quoted, or passed via --stdin.\n"
|
||||
return 0
|
||||
;;
|
||||
-v|--verbose)
|
||||
verbose=1
|
||||
shift
|
||||
;;
|
||||
-i|--stdin)
|
||||
read_stdin=1
|
||||
shift
|
||||
;;
|
||||
--)
|
||||
shift
|
||||
break
|
||||
;;
|
||||
-*)
|
||||
disp E "Invalid option '$1'. Use \"pwdscore --help\" to display usage (a password starting with '-' must follow '--')."
|
||||
return 1
|
||||
;;
|
||||
*)
|
||||
break
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if (( read_stdin )); then
|
||||
[[ $# -eq 0 ]] || {
|
||||
disp E "Do not pass a positional password when using --stdin."
|
||||
return 1
|
||||
}
|
||||
IFS= read -r password || true
|
||||
elif [[ $# -eq 0 ]]; then
|
||||
if [[ -t 0 ]]; then
|
||||
read -r -s -p 'Password: ' password < /dev/tty || true
|
||||
printf '\n' > /dev/tty
|
||||
else
|
||||
IFS= read -r password || true
|
||||
fi
|
||||
else
|
||||
[[ $# -eq 1 ]] || {
|
||||
disp E "Please provide exactly one password to score."
|
||||
return 1
|
||||
}
|
||||
password="$1"
|
||||
fi
|
||||
|
||||
local lower=${password,,}
|
||||
local length=${#password}
|
||||
local score=0
|
||||
local rating="very weak"
|
||||
local unique_count=0
|
||||
local i=0 idx=0
|
||||
local c1=0 c2=0 c3=0
|
||||
local ch=""
|
||||
local has_lower=0 has_upper=0 has_digit=0 has_symbol=0
|
||||
local pool_size=0
|
||||
local entropy_bits="0.0"
|
||||
local entropy_score=0
|
||||
local -A seen=()
|
||||
|
||||
if [[ -z $password ]]; then
|
||||
printf '1\n'
|
||||
return 0
|
||||
fi
|
||||
|
||||
if (( length >= 20 )); then
|
||||
score=40
|
||||
elif (( length >= 16 )); then
|
||||
score=34
|
||||
elif (( length >= 12 )); then
|
||||
score=28
|
||||
elif (( length >= 8 )); then
|
||||
score=18
|
||||
else
|
||||
score=$(( length * 2 ))
|
||||
fi
|
||||
|
||||
if [[ $password =~ [[:lower:]] ]]; then
|
||||
has_lower=1
|
||||
pool_size=$(( pool_size + 26 ))
|
||||
score=$(( score + 12 ))
|
||||
fi
|
||||
if [[ $password =~ [[:upper:]] ]]; then
|
||||
has_upper=1
|
||||
pool_size=$(( pool_size + 26 ))
|
||||
score=$(( score + 12 ))
|
||||
fi
|
||||
if [[ $password =~ [[:digit:]] ]]; then
|
||||
has_digit=1
|
||||
pool_size=$(( pool_size + 10 ))
|
||||
score=$(( score + 12 ))
|
||||
fi
|
||||
if [[ $password =~ [^[:alnum:]] ]]; then
|
||||
has_symbol=1
|
||||
pool_size=$(( pool_size + 33 ))
|
||||
score=$(( score + 14 ))
|
||||
fi
|
||||
|
||||
for (( i=0; i<length; i++ )); do
|
||||
ch=${password:i:1}
|
||||
if [[ -z ${seen["$ch"]+x} ]]; then
|
||||
seen["$ch"]=1
|
||||
unique_count=$(( unique_count + 1 ))
|
||||
fi
|
||||
done
|
||||
score=$(( score + (unique_count * 10) / length ))
|
||||
|
||||
if (( pool_size > 1 )); then
|
||||
entropy_bits=$(awk -v len="$length" -v pool="$pool_size" \
|
||||
'BEGIN { printf "%.1f", len * (log(pool) / log(2)) }')
|
||||
|
||||
entropy_score=$(awk -v bits="$entropy_bits" 'BEGIN {
|
||||
if (bits < 28) print -35;
|
||||
else if (bits < 36) print -25;
|
||||
else if (bits < 60) print -10;
|
||||
else if (bits < 80) print 0;
|
||||
else if (bits < 100) print 5;
|
||||
else print 10;
|
||||
}')
|
||||
score=$(( score + entropy_score ))
|
||||
fi
|
||||
|
||||
if [[ $lower =~ (password|admin|root|qwerty|azerty|welcome|letmein|secret|changeme) ]]; then
|
||||
score=$(( score - 25 ))
|
||||
fi
|
||||
if [[ $lower =~ (1234|abcd|qwer|0000|1111|aaaa) ]]; then
|
||||
score=$(( score - 15 ))
|
||||
fi
|
||||
if [[ $password =~ (.)\1\1 ]]; then
|
||||
score=$(( score - 10 ))
|
||||
fi
|
||||
if (( length < 8 )); then
|
||||
score=$(( score - 10 ))
|
||||
fi
|
||||
if (( unique_count * 2 < length )); then
|
||||
score=$(( score - 10 ))
|
||||
fi
|
||||
|
||||
for (( idx=0; idx<length-2; idx++ )); do
|
||||
printf -v c1 '%d' "'${lower:idx:1}"
|
||||
printf -v c2 '%d' "'${lower:idx+1:1}"
|
||||
printf -v c3 '%d' "'${lower:idx+2:1}"
|
||||
if (( (c2 == c1 + 1 && c3 == c2 + 1) || \
|
||||
(c2 == c1 - 1 && c3 == c2 - 1) )); then
|
||||
score=$(( score - 10 ))
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
if (( score < 1 )); then
|
||||
score=1
|
||||
elif (( score > 100 )); then
|
||||
score=100
|
||||
fi
|
||||
|
||||
if (( score >= 90 )); then
|
||||
rating='excellent'
|
||||
elif (( score >= 75 )); then
|
||||
rating='strong'
|
||||
elif (( score >= 60 )); then
|
||||
rating='good'
|
||||
elif (( score >= 40 )); then
|
||||
rating='fair'
|
||||
elif (( score >= 20 )); then
|
||||
rating='weak'
|
||||
fi
|
||||
|
||||
if (( verbose )); then
|
||||
printf 'Score: %d/100\n' "$score"
|
||||
printf 'Rating: %s\n' "$rating"
|
||||
printf 'Length: %d\n' "$length"
|
||||
printf 'Lowercase: %s\n' "$([[ $has_lower -eq 1 ]] && echo yes || echo no)"
|
||||
printf 'Uppercase: %s\n' "$([[ $has_upper -eq 1 ]] && echo yes || echo no)"
|
||||
printf 'Digits: %s\n' "$([[ $has_digit -eq 1 ]] && echo yes || echo no)"
|
||||
printf 'Symbols: %s\n' "$([[ $has_symbol -eq 1 ]] && echo yes || echo no)"
|
||||
printf 'Unique chars: %d\n' "$unique_count"
|
||||
printf 'Entropy: ~%s bits\n' "$entropy_bits"
|
||||
printf 'Entropy modifier: %+d\n' "$entropy_score"
|
||||
else
|
||||
printf '%d\n' "$score"
|
||||
fi
|
||||
}
|
||||
export -f pwdscore
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
# EOF
|
||||
|
||||
+363
-105
@@ -1,119 +1,377 @@
|
||||
#!/usr/bin/env bash
|
||||
# ------------------------------------------------------------------------------
|
||||
# Let the rain fall
|
||||
# Copyright (c) 2013-2026 Geoffray Levasseur <fatalerrors@geoffray-levasseur.org>
|
||||
# Protected by the BSD3 license. Please read bellow for details.
|
||||
#
|
||||
# * Redistribution and use in source and binary forms,
|
||||
# * with or without modification, are permitted provided
|
||||
# * that the following conditions are met:
|
||||
# *
|
||||
# * Redistributions of source code must retain the above
|
||||
# * copyright notice, this list of conditions and the
|
||||
# * following disclaimer.
|
||||
# *
|
||||
# * Redistributions in binary form must reproduce the above
|
||||
# * copyright notice, this list of conditions and the following
|
||||
# * disclaimer in the documentation and/or other materials
|
||||
# * provided with the distribution.
|
||||
# *
|
||||
# * Neither the name of the copyright holder nor the names
|
||||
# * of any other contributors may be used to endorse or
|
||||
# * promote products derived from this software without
|
||||
# * specific prior written permission.
|
||||
# *
|
||||
# * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
|
||||
# * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
|
||||
# * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
|
||||
# * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
# * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
|
||||
# * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
# * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
|
||||
# * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
# * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||
# * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
|
||||
# * OF SUCH DAMAGE.
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
load_conf "rain"
|
||||
|
||||
# Common helpers, then the ones private to this module (helpers/rain.sh)
|
||||
# shellcheck source=../helpers/common.sh
|
||||
declare -F _profile_load_helpers >/dev/null || . "$(dirname -- "${BASH_SOURCE[0]}")/../helpers/common.sh"
|
||||
_profile_load_helpers "${BASH_SOURCE[0]}"
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# Let the rain fall (current style)
|
||||
# Usage: rain [OPTIONS]
|
||||
rain()
|
||||
{
|
||||
local exit_st=0
|
||||
local rain_cars=("|" "│" "┃" "┆" "┇" "┊" "┋" "╽" "╿")
|
||||
local rain_colors=("\e[37m" "\e[37;1m")
|
||||
# More from 256 color mode
|
||||
for i in {244..255}; do
|
||||
rain_colors=( "${rain_colors[@]}" "\e[38;5;${i}m" )
|
||||
done
|
||||
local rain_tab=${#rain_cars[@]}
|
||||
local rain_color_tab=${#rain_colors[@]}
|
||||
local num_rain_metadata=5
|
||||
local term_height=$(tput lines)
|
||||
local term_width=$(tput cols)
|
||||
local step_duration=0.050
|
||||
local X=0 Y=0 drop_length=0 rain_drop=0
|
||||
local max_rain_width=0 new_rain_odd=0 falling_odd=0
|
||||
|
||||
|
||||
sigwinch() {
|
||||
term_width=$(tput cols)
|
||||
term_height=$(tput lines)
|
||||
#step_duration=0.025
|
||||
(( max_rain_width = term_width * term_height / 4 ))
|
||||
(( max_rain_height = term_height < 10 ? 1 : term_height / 10 ))
|
||||
# In percentage
|
||||
(( new_rain_odd = term_height > 50 ? 100 : term_height * 2 ))
|
||||
(( new_rain_odd = new_rain_odd * 75 / 100 ))
|
||||
(( falling_odd = term_height > 25 ? 100 : term_height * 4 ))
|
||||
(( falling_odd = falling_odd * 90 / 100 ))
|
||||
local _rain_show_usage
|
||||
_rain_show_usage()
|
||||
{
|
||||
printf "Usage: rain [OPTIONS]\n"
|
||||
printf "Options:\n"
|
||||
printf "\t-s, --speed NUM\tSet speed value (default: 5 => 0.050s).\n"
|
||||
printf "\t\t\t\tValues >=1 use a /100 scale (5 => 0.05s).\n"
|
||||
printf "\t\t\t\tValues <1 are interpreted as raw seconds.\n"
|
||||
printf "\t-d, --density NUM\tMaximum number of simultaneous falling elements.\n"
|
||||
printf "\t-c, --color COLOR\tSet the color theme (default: white).\n"
|
||||
printf "\t-h, --help\t\tDisplay this help message and exit.\n\n"
|
||||
printf "Available Colors (case-insensitive):\n"
|
||||
printf "\t\e[32mgreen\e[0m\t: Matrix-like green shades\n"
|
||||
printf "\t\e[34mblue\e[0m\t: Deep ocean blue gradients\n"
|
||||
printf "\t\e[31mred\e[0m\t: Crimson/Blood rain\n"
|
||||
printf "\t\e[33myellow\e[0m\t: Amber and gold tones\n"
|
||||
printf "\t\e[36mcyan\e[0m\t: Electric cyan/turquoise\n"
|
||||
printf "\twhite\t: Greyscale and white (original style)\n\n"
|
||||
printf "Press q (or Ctrl-C) to quit.\n"
|
||||
printf "Example: rain --color green --speed 3\n"
|
||||
}
|
||||
|
||||
do_exit() {
|
||||
exit_st=1
|
||||
}
|
||||
local _raw_speed="${RAIN_DEFAULT_SPEED:-5}"
|
||||
local step_duration
|
||||
step_duration=$(_rain_normalize_speed "$_raw_speed") || step_duration=0.050
|
||||
local base_color="${RAIN_DEFAULT_COLOR:-white}"
|
||||
local density_override="${RAIN_DEFAULT_DENSITY:-}"
|
||||
|
||||
do_render() {
|
||||
# Clean screen first
|
||||
local idx=0
|
||||
for ((idx = 0; idx < num_rains * num_rain_metadata; idx += num_rain_metadata)); do
|
||||
X=${rains[idx]}
|
||||
Y=${rains[idx + 1]}
|
||||
drop_length=${rains[idx + 4]}
|
||||
for ((y = Y; y < Y + drop_length; y++)); do
|
||||
(( y < 1 || y > term_height )) && continue
|
||||
echo -ne "\e[${y};${X}H "
|
||||
done
|
||||
done
|
||||
if [[ -n "$density_override" ]]; then
|
||||
density_override=$(_rain_normalize_density "$density_override") || density_override=""
|
||||
fi
|
||||
|
||||
for ((idx = 0; idx < num_rains * num_rain_metadata; idx += num_rain_metadata)); do
|
||||
if (( 100 * RANDOM / 32768 < falling_odd )); then
|
||||
# Falling
|
||||
if (( ++rains[idx + 1] > term_height )); then
|
||||
# Out of screen, bye sweet <3
|
||||
rains=("${rains[@]:0:idx}"
|
||||
"${rains[@]:idx+num_rain_metadata:num_rains*num_rain_metadata}")
|
||||
(( num_rains-- ))
|
||||
continue
|
||||
while [[ "$#" -gt 0 ]]; do
|
||||
case $1 in
|
||||
-s|--speed)
|
||||
if [[ -n "$2" && ! "$2" =~ ^- ]]; then
|
||||
step_duration=$(_rain_normalize_speed "$2") || {
|
||||
disp E "--speed requires a numeric value greater than 0."
|
||||
_rain_show_usage
|
||||
return 1
|
||||
}
|
||||
shift
|
||||
else
|
||||
disp E "--speed requires a numeric value greater than 0."
|
||||
_rain_show_usage
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
X=${rains[idx]}
|
||||
Y=${rains[idx + 1]}
|
||||
rain_drop=${rains[idx + 2]}
|
||||
drop_color=${rains[idx + 3]}
|
||||
drop_length=${rains[idx + 4]}
|
||||
for ((y = Y; y < Y + drop_length; y++)); do
|
||||
(( y < 1 || y > term_height )) && continue
|
||||
echo -ne "\e[${y};${X}H${drop_color}${rain_drop}"
|
||||
done
|
||||
done
|
||||
}
|
||||
|
||||
trap do_exit TERM INT
|
||||
trap sigwinch WINCH
|
||||
# No echo stdin and hide the cursor
|
||||
stty -echo
|
||||
echo -ne "\e[?25l"
|
||||
|
||||
echo -ne "\e[2J"
|
||||
local rains=()
|
||||
local num_rains=0
|
||||
sigwinch
|
||||
while (( exit_st <= 0 )); do
|
||||
if (( $exit_st <=0 )); then
|
||||
read -n 1 -t $step_duration ch
|
||||
case "$ch" in
|
||||
q|Q)
|
||||
do_exit
|
||||
;;
|
||||
esac
|
||||
|
||||
if (( num_rains < max_rain_width )) && (( 100 * RANDOM / 32768 < new_rain_odd )); then
|
||||
# Need new |, 1-based
|
||||
rain_drop="${rain_cars[rain_tab * RANDOM / 32768]}"
|
||||
drop_color="${rain_colors[rain_color_tab * RANDOM / 32768]}"
|
||||
drop_length=$(( max_rain_height * RANDOM / 32768 + 1 ))
|
||||
X=$(( term_width * RANDOM / 32768 + 1 ))
|
||||
Y=$(( 1 - drop_length ))
|
||||
rains=( "${rains[@]}" "$X" "$Y" "$rain_drop" "$drop_color" "$drop_length" )
|
||||
(( num_rains++ ))
|
||||
fi
|
||||
|
||||
# Let rain fall!
|
||||
do_render
|
||||
fi
|
||||
-c|--color)
|
||||
if [[ -n "$2" && ! "$2" =~ ^- ]]; then
|
||||
base_color="$2"
|
||||
shift
|
||||
else
|
||||
disp E "--color requires a color name."
|
||||
_rain_show_usage
|
||||
return 1
|
||||
fi
|
||||
;;
|
||||
-d|--density)
|
||||
if [[ -n "$2" && ! "$2" =~ ^- ]]; then
|
||||
density_override=$(_rain_normalize_density "$2") || {
|
||||
disp E "--density requires a positive integer value."
|
||||
_rain_show_usage
|
||||
return 1
|
||||
}
|
||||
shift
|
||||
else
|
||||
disp E "--density requires a positive integer value."
|
||||
_rain_show_usage
|
||||
return 1
|
||||
fi
|
||||
;;
|
||||
-h|--help)
|
||||
_rain_show_usage
|
||||
return 0
|
||||
;;
|
||||
--)
|
||||
shift
|
||||
break
|
||||
;;
|
||||
*)
|
||||
disp E "Unknown option: $1"
|
||||
_rain_show_usage
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
shift
|
||||
done
|
||||
echo -ne "\e[${term_height};1H\e[0K"
|
||||
|
||||
# Show cursor and echo stdin
|
||||
echo -ne "\e[?25h"
|
||||
stty echo
|
||||
unset exit_st
|
||||
trap - TERM INT
|
||||
trap - WINCH
|
||||
_rain_engine "$step_duration" "$base_color" "rain" "" "$density_override"
|
||||
}
|
||||
export -f rain
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# Matrix style digital rain
|
||||
# Usage: matrix [OPTIONS]
|
||||
matrix()
|
||||
{
|
||||
local _matrix_show_usage
|
||||
_matrix_show_usage()
|
||||
{
|
||||
printf "Usage: matrix [OPTIONS]\n"
|
||||
printf "Options:\n"
|
||||
printf "\t-s, --speed NUM\tSet speed value (default: 3.5 => 0.035s).\n"
|
||||
printf "\t\t\t\tValues >=1 use a /100 scale (3.5 => 0.035s).\n"
|
||||
printf "\t\t\t\tValues <1 are interpreted as raw seconds.\n"
|
||||
printf "\t-d, --density NUM\tMaximum number of simultaneous falling elements.\n"
|
||||
printf "\t-c, --color COLOR\tSet color theme (default: green).\n"
|
||||
printf "\t-C, --charset SET\tCharacter set: binary, kana, ascii (default: binary).\n"
|
||||
printf "\t-h, --help\t\tDisplay this help message and exit.\n\n"
|
||||
printf "Press q (or Ctrl-C) to quit.\n"
|
||||
printf "Example: matrix -C kana -c green --speed 2\n"
|
||||
}
|
||||
|
||||
local _raw_speed="${MATRIX_DEFAULT_SPEED:-3.5}"
|
||||
local step_duration
|
||||
step_duration=$(_rain_normalize_speed "$_raw_speed") || step_duration=0.035
|
||||
local base_color="${MATRIX_DEFAULT_COLOR:-green}"
|
||||
# Lower-cased, like the value of -C: the configuration may say "Kana"
|
||||
local charset="${MATRIX_DEFAULT_CHARSET:-binary}"
|
||||
charset="${charset,,}"
|
||||
local density_override="${MATRIX_DEFAULT_DENSITY:-}"
|
||||
|
||||
if [[ -n "$density_override" ]]; then
|
||||
density_override=$(_rain_normalize_density "$density_override") || density_override=""
|
||||
fi
|
||||
|
||||
while [[ "$#" -gt 0 ]]; do
|
||||
case $1 in
|
||||
-s|--speed)
|
||||
if [[ -n "$2" && ! "$2" =~ ^- ]]; then
|
||||
step_duration=$(_rain_normalize_speed "$2") || {
|
||||
disp E "--speed requires a numeric value greater than 0."
|
||||
_matrix_show_usage
|
||||
return 1
|
||||
}
|
||||
shift
|
||||
else
|
||||
disp E "--speed requires a numeric value greater than 0."
|
||||
_matrix_show_usage
|
||||
return 1
|
||||
fi
|
||||
;;
|
||||
-c|--color)
|
||||
if [[ -n "$2" && ! "$2" =~ ^- ]]; then
|
||||
case "${2,,}" in
|
||||
binary|kana|kanji|ascii)
|
||||
disp W "'${2}' looks like a charset value. Use -C/--charset for clarity."
|
||||
charset="${2,,}"
|
||||
;;
|
||||
*)
|
||||
base_color="$2"
|
||||
;;
|
||||
esac
|
||||
shift
|
||||
else
|
||||
disp E "--color requires a color name."
|
||||
_matrix_show_usage
|
||||
return 1
|
||||
fi
|
||||
;;
|
||||
-C|--charset)
|
||||
if [[ -n "$2" && ! "$2" =~ ^- ]]; then
|
||||
charset="${2,,}"
|
||||
shift
|
||||
else
|
||||
disp E "--charset requires a value."
|
||||
_matrix_show_usage
|
||||
return 1
|
||||
fi
|
||||
;;
|
||||
-d|--density)
|
||||
if [[ -n "$2" && ! "$2" =~ ^- ]]; then
|
||||
density_override=$(_rain_normalize_density "$2") || {
|
||||
disp E "--density requires a positive integer value."
|
||||
_matrix_show_usage
|
||||
return 1
|
||||
}
|
||||
shift
|
||||
else
|
||||
disp E "--density requires a positive integer value."
|
||||
_matrix_show_usage
|
||||
return 1
|
||||
fi
|
||||
;;
|
||||
-h|--help)
|
||||
_matrix_show_usage
|
||||
return 0
|
||||
;;
|
||||
--)
|
||||
shift
|
||||
break
|
||||
;;
|
||||
*)
|
||||
disp E "Unknown option: $1"
|
||||
_matrix_show_usage
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
shift
|
||||
done
|
||||
|
||||
_rain_engine "$step_duration" "$base_color" "matrix" "$charset" "$density_override"
|
||||
}
|
||||
export -f matrix
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# Full-screen rainbow screensaver
|
||||
# Usage: rainbow [OPTIONS]
|
||||
rainbow()
|
||||
{
|
||||
local PARSED
|
||||
|
||||
PARSED=$(getopt -o hs: --long help,speed: -n 'rainbow' -- "$@")
|
||||
# shellcheck disable=SC2181 # getopt return code is checked immediately after
|
||||
if [[ $? -ne 0 ]]; then
|
||||
disp E "Invalid options, use \"rainbow --help\" to display usage."
|
||||
return 1
|
||||
fi
|
||||
|
||||
local _raw_speed="${RAINBOW_DEFAULT_SPEED:-4}"
|
||||
local frame_sleep
|
||||
frame_sleep=$(_rain_normalize_speed "$_raw_speed") || frame_sleep=0.040
|
||||
|
||||
# Restore the getopt-normalised (shell-quoted) argument list
|
||||
local -a _pargs=()
|
||||
_getopt_split _pargs "$PARSED"
|
||||
set -- "${_pargs[@]}"
|
||||
while true; do
|
||||
case "$1" in
|
||||
-h|--help)
|
||||
printf "Usage: rainbow [OPTIONS]\n"
|
||||
printf "Options:\n"
|
||||
printf "\t-s, --speed NUM\tSet horizontal color shift speed (default: 4 => 0.040s).\n"
|
||||
printf "\t\t\t\tValues >=1 use a /100 scale (4 => 0.04s).\n"
|
||||
printf "\t\t\t\tValues <1 are interpreted as raw seconds.\n"
|
||||
printf "\t-h, --help\t\tDisplay this help message and exit.\n\n"
|
||||
printf "The rainbow fills the whole terminal with background colors only.\n"
|
||||
printf "It uses truecolor when supported, otherwise ANSI bright backgrounds.\n"
|
||||
printf "Press q (or Ctrl-C) to quit.\n"
|
||||
return 0
|
||||
;;
|
||||
-s|--speed)
|
||||
frame_sleep=$(_rain_normalize_speed "$2") || {
|
||||
disp E "--speed requires a numeric value greater than 0."
|
||||
return 1
|
||||
}
|
||||
shift 2
|
||||
;;
|
||||
--)
|
||||
shift
|
||||
break
|
||||
;;
|
||||
*)
|
||||
disp E "Invalid options, use \"rainbow --help\" to display usage."
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
_rain_need_tty rainbow || return 1
|
||||
|
||||
local exit_st=0
|
||||
local term_height=0 term_width=0
|
||||
local use_truecolor=0
|
||||
local offset=0 row=0 col=0 palette_width=0 idx=0
|
||||
local row_line="" screen="" ch=""
|
||||
# shellcheck disable=SC2034 # used by _rain_enter/_rain_leave (dynamic scoping)
|
||||
local saved_stty="" saved_traps=""
|
||||
|
||||
# WINCH handler (and initial sizing): rebuild the palette for the new width
|
||||
_rainbow_sigwinch()
|
||||
{
|
||||
term_width=$(tput cols)
|
||||
term_height=$(tput lines)
|
||||
((term_width < 1)) && term_width=1
|
||||
((term_height < 1)) && term_height=1
|
||||
|
||||
if [[ "$TERM" == *direct* || "${COLORTERM:-}" == "truecolor" || "${COLORTERM:-}" == "24bit" ]]; then
|
||||
use_truecolor=1
|
||||
else
|
||||
use_truecolor=0
|
||||
fi
|
||||
|
||||
palette_width=$term_width
|
||||
offset=0
|
||||
_rainbow_build_palette "$palette_width" "$use_truecolor"
|
||||
}
|
||||
|
||||
_rain_enter _rainbow_sigwinch
|
||||
_rainbow_sigwinch
|
||||
while ((exit_st <= 0)); do
|
||||
ch=""
|
||||
# A timeout makes read fail (status > 128): expected, not an error
|
||||
read -r -n 1 -t "$frame_sleep" ch || true
|
||||
case "$ch" in
|
||||
q|Q)
|
||||
_rain_stop
|
||||
;;
|
||||
esac
|
||||
|
||||
# One row of colours, shifted by offset (col - offset is at least
|
||||
# 1 - palette_width, so adding palette_width makes it positive)
|
||||
row_line=""
|
||||
for ((col = 0; col < term_width; col++)); do
|
||||
idx=$(( (col - offset + palette_width) % palette_width ))
|
||||
row_line+="${RAINBOW_BG_PALETTE[idx]} "
|
||||
done
|
||||
row_line+=$'\e[0m'
|
||||
|
||||
# Every row is the same: the whole screen goes out in one write
|
||||
screen=""
|
||||
for ((row = 1; row <= term_height; row++)); do
|
||||
screen+="\e[${row};1H${row_line}"
|
||||
done
|
||||
printf '%b' "$screen"
|
||||
|
||||
offset=$(( (offset + 1) % palette_width ))
|
||||
done
|
||||
|
||||
_rain_leave
|
||||
unset -f _rainbow_sigwinch
|
||||
unset RAINBOW_BG_PALETTE
|
||||
return 0
|
||||
}
|
||||
export -f rainbow
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
# EOF
|
||||
|
||||
+271
-54
@@ -1,70 +1,287 @@
|
||||
#!/usr/bin/env bash
|
||||
# ------------------------------------------------------------------------------
|
||||
# Remove host from know_host (name and IP) for the active user
|
||||
# Copyright (c) 2013-2026 Geoffray Levasseur <fatalerrors@geoffray-levasseur.org>
|
||||
# Protected by the BSD3 license. Please read bellow for details.
|
||||
#
|
||||
# * Redistribution and use in source and binary forms,
|
||||
# * with or without modification, are permitted provided
|
||||
# * that the following conditions are met:
|
||||
# *
|
||||
# * Redistributions of source code must retain the above
|
||||
# * copyright notice, this list of conditions and the
|
||||
# * following disclaimer.
|
||||
# *
|
||||
# * Redistributions in binary form must reproduce the above
|
||||
# * copyright notice, this list of conditions and the following
|
||||
# * disclaimer in the documentation and/or other materials
|
||||
# * provided with the distribution.
|
||||
# *
|
||||
# * Neither the name of the copyright holder nor the names
|
||||
# * of any other contributors may be used to endorse or
|
||||
# * promote products derived from this software without
|
||||
# * specific prior written permission.
|
||||
# *
|
||||
# * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
|
||||
# * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
|
||||
# * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
|
||||
# * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
# * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
|
||||
# * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
# * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
|
||||
# * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
# * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||
# * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
|
||||
# * OF SUCH DAMAGE.
|
||||
# ------------------------------------------------------------------------------
|
||||
rmhost ()
|
||||
|
||||
load_conf "ssh"
|
||||
|
||||
# Common helpers, then the ones private to this module (helpers/ssh.sh)
|
||||
# shellcheck source=../helpers/common.sh
|
||||
declare -F _profile_load_helpers >/dev/null || . "$(dirname -- "${BASH_SOURCE[0]}")/../helpers/common.sh"
|
||||
_profile_load_helpers "${BASH_SOURCE[0]}"
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# Remove host entries (name and IP) from ~/.ssh/known_hosts for the active user
|
||||
# Usage: rmhost <hostname|ip> [hostname2|ip2 [...]]
|
||||
rmhost()
|
||||
{
|
||||
if [[ "$#" -lt 1 ]]; then
|
||||
echo "Error: incorrect number of parameters."
|
||||
echo "Usage: rmhost <hostname|ip> [hostname2|ip2 [...]]"
|
||||
local PARSED
|
||||
local all_users=0
|
||||
local -a known_hosts_files=()
|
||||
|
||||
PARSED=$(getopt -o ha --long help,all-users -n 'rmhost' -- "$@")
|
||||
# shellcheck disable=SC2181 # getopt return code is checked immediately after
|
||||
if [[ $? -ne 0 ]]; then
|
||||
disp E "Invalid options, use \"rmhost --help\" to display usage."
|
||||
return 1
|
||||
fi
|
||||
|
||||
while [[ $1 ]]; do
|
||||
local hst=$1 && shift
|
||||
isipv4 $hst > /dev/null
|
||||
local v4=$?
|
||||
isipv6 $hst > /dev/null
|
||||
local v6=$?
|
||||
|
||||
if [[ $v4 -eq 0 || $v6 -eq 0 ]]; then
|
||||
local ip=$hst
|
||||
unset hst
|
||||
fi
|
||||
unset v4 v6
|
||||
|
||||
if [[ ! $ip && $hst ]]; then
|
||||
ip=$(host $hst | grep "has address" | awk '{print $NF}')
|
||||
[[ ! $? ]] &&
|
||||
echo "*** rmhost(): Error extracting IP from hostname." &&
|
||||
return 1
|
||||
fi
|
||||
|
||||
if [[ $hst ]]; then
|
||||
echo "Removing host $hst from ssh known_host..."
|
||||
ssh-keygen -R $hst > /dev/null
|
||||
fi
|
||||
if [[ $ip ]]; then
|
||||
echo "Removing IP $ip from ssh known_host..."
|
||||
ssh-keygen -R $ip > /dev/null
|
||||
fi
|
||||
unset hst ip
|
||||
done
|
||||
}
|
||||
export -f rmhost
|
||||
# Restore the getopt-normalised (shell-quoted) argument list
|
||||
local -a _pargs=()
|
||||
_getopt_split _pargs "$PARSED"
|
||||
set -- "${_pargs[@]}"
|
||||
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# Login root via SSH on the given machine
|
||||
# ------------------------------------------------------------------------------
|
||||
ssr ()
|
||||
{
|
||||
for opt in $@ ; do
|
||||
case $opt in
|
||||
"-h"|"--help")
|
||||
echo "ssr: do a root user ssh login."
|
||||
echo
|
||||
echo "Usage: ssr <server [ssh options]>"
|
||||
while true; do
|
||||
case "$1" in
|
||||
-h|--help)
|
||||
printf "rmhost: Remove host/IP from known_hosts files.\n\n"
|
||||
printf "Usage: rmhost [--all-users] <pattern|ip> [pattern2|ip2 ...]\n\n"
|
||||
printf "Options:\n"
|
||||
printf " -a, --all-users Remove entries from all local users when run as root\n"
|
||||
printf " -h, --help Display this help screen\n\n"
|
||||
printf "A host name is removed together with all the IP addresses it resolves to.\n"
|
||||
printf "Wildcards:\n"
|
||||
printf " Glob patterns (*, ?, [...]) are expanded against unhashed known_hosts entries.\n"
|
||||
printf " Hashed entries (prefixed with |1|) are never matched by wildcards.\n"
|
||||
return 0
|
||||
;;
|
||||
-a|--all-users)
|
||||
all_users=1
|
||||
shift
|
||||
;;
|
||||
--)
|
||||
shift
|
||||
break
|
||||
;;
|
||||
*)
|
||||
disp E "Invalid options, use \"rmhost --help\" to display usage."
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
[[ ! $1 ]] &&
|
||||
echo "Please specify the server you want to log in." &&
|
||||
[[ $# -eq 0 ]] && {
|
||||
disp E "Missing argument. Use 'rmhost --help' for usage."
|
||||
return 1
|
||||
}
|
||||
|
||||
local srv=$1 && shift
|
||||
command -v ssh-keygen >/dev/null 2>&1 || {
|
||||
disp E "ssh-keygen is not installed."
|
||||
return 127
|
||||
}
|
||||
|
||||
ssh -Y root@$srv $@
|
||||
# A name starting with "-" would be read as an option by the tools below
|
||||
local target
|
||||
for target in "$@"; do
|
||||
if [[ "$target" == -* ]]; then
|
||||
disp E "Invalid host '$target'."
|
||||
return 1
|
||||
fi
|
||||
done
|
||||
|
||||
if (( all_users )); then
|
||||
[[ ${EUID:-$(id -u)} -eq 0 ]] || {
|
||||
disp E "Option --all-users is only available when run as root."
|
||||
return 1
|
||||
}
|
||||
|
||||
# Users may share a home directory: each file is listed once
|
||||
local -A _seen_homes=()
|
||||
local home
|
||||
while IFS=: read -r _ _ _ _ _ home _; do
|
||||
[[ -n $home && -f $home/.ssh/known_hosts && -z ${_seen_homes[$home]+x} ]] || continue
|
||||
_seen_homes[$home]=1
|
||||
known_hosts_files+=("$home/.ssh/known_hosts")
|
||||
done < /etc/passwd
|
||||
|
||||
[[ -f /etc/ssh/ssh_known_hosts ]] && \
|
||||
known_hosts_files+=("/etc/ssh/ssh_known_hosts")
|
||||
|
||||
[[ ${#known_hosts_files[@]} -gt 0 ]] || {
|
||||
disp W "No known_hosts files found for local users."
|
||||
return 0
|
||||
}
|
||||
else
|
||||
if [[ ! -f "${HOME}/.ssh/known_hosts" ]]; then
|
||||
disp W "No known_hosts file found (${HOME}/.ssh/known_hosts)."
|
||||
return 0
|
||||
fi
|
||||
known_hosts_files=("${HOME}/.ssh/known_hosts")
|
||||
fi
|
||||
|
||||
for target in "$@"; do
|
||||
# Wildcard: expand the glob pattern against the unhashed entries of
|
||||
# each file, and remove the matches from that file only. A "[host]:port"
|
||||
# entry is not a pattern: its brackets are known_hosts notation.
|
||||
if [[ "$target" == *['*?']* ]] || \
|
||||
[[ "$target" == *'['* && ! "$target" =~ ^\[[^][]+\]:[0-9]+$ ]]; then
|
||||
local -a _matched=() _wentries=()
|
||||
local _wf _wl _wfield _whost _wmatch _key _found=0
|
||||
for _wf in "${known_hosts_files[@]}"; do
|
||||
_matched=()
|
||||
while IFS= read -r _wl; do
|
||||
# Comments, hashed entries and @cert-authority/@revoked
|
||||
# marker lines are left alone
|
||||
[[ -z "$_wl" || "$_wl" == '#'* || "$_wl" == '|'* || "$_wl" == '@'* ]] && continue
|
||||
_wfield="${_wl%% *}"
|
||||
IFS=',' read -ra _wentries <<< "$_wfield"
|
||||
for _whost in "${_wentries[@]}"; do
|
||||
# Strip [host]:port notation to get the bare name for matching
|
||||
if [[ "$_whost" == '['*']:'* ]]; then
|
||||
_wmatch="${_whost#[}"
|
||||
_wmatch="${_wmatch%%]:*}"
|
||||
else
|
||||
_wmatch="$_whost"
|
||||
fi
|
||||
# shellcheck disable=SC2053 # unquoted on purpose: glob match
|
||||
[[ "$_wmatch" == $target ]] && _matched+=("$_whost")
|
||||
done
|
||||
done < "$_wf"
|
||||
|
||||
[[ ${#_matched[@]} -gt 0 ]] || continue
|
||||
_found=1
|
||||
while IFS= read -r _key; do
|
||||
disp I "Removing '$_key' from $_wf..."
|
||||
if ! _rmhost_remove "$_key" "$_wf"; then
|
||||
disp W "No known_hosts entry found for '$_key' in '$_wf'."
|
||||
fi
|
||||
done < <(printf '%s\n' "${_matched[@]}" | sort -u)
|
||||
done
|
||||
|
||||
(( _found )) || disp W "No known_hosts entries match pattern '$target'."
|
||||
continue
|
||||
fi
|
||||
|
||||
local hst="$target"
|
||||
local ip
|
||||
local -a ips=()
|
||||
|
||||
if isipv4 "$hst" >/dev/null 2>&1 || isipv6 "$hst" >/dev/null 2>&1; then
|
||||
ips=("$hst")
|
||||
hst=""
|
||||
elif [[ "$hst" == '['*']:'* ]]; then
|
||||
# Explicit [host]:port entry: removed exactly as given
|
||||
:
|
||||
else
|
||||
# All the addresses of the name: ssh records the one it connected
|
||||
# to, which may be any of them (IPv4 and IPv6, several A records)
|
||||
if command -v host >/dev/null 2>&1; then
|
||||
mapfile -t ips < <(host "$hst" 2>/dev/null |
|
||||
awk '/has address|has IPv6 address/ {print $NF}' | sort -u)
|
||||
elif command -v getent >/dev/null 2>&1; then
|
||||
# ahosts lists every address once per socket type
|
||||
mapfile -t ips < <(getent ahosts "$hst" 2>/dev/null |
|
||||
awk '{print $1}' | sort -u)
|
||||
else
|
||||
disp W "No resolver tool found; removing hostname only for '$hst'."
|
||||
fi
|
||||
|
||||
[[ ${#ips[@]} -eq 0 ]] && \
|
||||
disp W "Could not resolve IP for '$hst'; removing hostname only."
|
||||
fi
|
||||
|
||||
local known_hosts_file=""
|
||||
for known_hosts_file in "${known_hosts_files[@]}"; do
|
||||
if [[ -n ${hst:-} ]]; then
|
||||
disp I "Removing host $hst from $known_hosts_file..."
|
||||
if ! _rmhost_remove "$hst" "$known_hosts_file"; then
|
||||
disp W "No known_hosts entry found for '$hst' in '$known_hosts_file'."
|
||||
fi
|
||||
fi
|
||||
for ip in "${ips[@]}"; do
|
||||
disp I "Removing IP $ip from $known_hosts_file..."
|
||||
if ! _rmhost_remove "$ip" "$known_hosts_file"; then
|
||||
disp W "No known_hosts entry found for '$ip' in '$known_hosts_file'."
|
||||
fi
|
||||
done
|
||||
done
|
||||
done
|
||||
}
|
||||
export -f rmhost
|
||||
# ------------------------------------------------------------------------------
|
||||
# Login root via SSH on the given machine
|
||||
# Usage: ssr <server [ssh options]>
|
||||
ssr()
|
||||
{
|
||||
case "${1:-}" in
|
||||
-h|--help)
|
||||
printf "ssr: SSH into a server as root.\n\n"
|
||||
printf "Usage: ssr <server> [ssh_options...]\n\n"
|
||||
printf "Notes:\n"
|
||||
printf " The first argument is the target server.\n"
|
||||
printf " All remaining arguments are passed directly to ssh.\n\n"
|
||||
printf "Examples:\n"
|
||||
printf " ssr srv01\n"
|
||||
printf " ssr srv01 -p 2222\n"
|
||||
printf " ssr srv01 -i ~/.ssh/id_ed25519 -J bastion\n"
|
||||
return 0
|
||||
;;
|
||||
esac
|
||||
|
||||
command -v ssh >/dev/null 2>&1 || {
|
||||
disp E "ssh is not installed."
|
||||
return 127
|
||||
}
|
||||
|
||||
[[ $# -eq 0 || -z ${1:-} ]] && {
|
||||
disp E "Please specify the server you want to log in."
|
||||
return 1
|
||||
}
|
||||
|
||||
local srv=$1
|
||||
shift
|
||||
|
||||
# The first argument is the server: an option here ("ssr -p 22 host")
|
||||
# would end up in the root@... destination
|
||||
if [[ $srv == -* ]]; then
|
||||
disp E "'$srv' is not a server name: the server comes first, then the ssh options."
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Build the default options array from the configuration. An unset
|
||||
# SSH_DEFAULT_OPT means -Y (X11 forwarding); an empty one means no default
|
||||
# option at all, as the example configuration documents.
|
||||
local -a ssh_default_opts=()
|
||||
if [[ -n "${SSH_DEFAULT_OPT+x}" ]]; then
|
||||
read -ra ssh_default_opts <<< "${SSH_DEFAULT_OPT}"
|
||||
else
|
||||
ssh_default_opts=(-Y)
|
||||
fi
|
||||
|
||||
# shellcheck disable=SC2029
|
||||
ssh "${ssh_default_opts[@]}" root@"$srv" "$@"
|
||||
}
|
||||
export -f ssr
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
# EOF
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
# Abyss prompt theme — deep ocean navy, electric teal, golden accents
|
||||
# ------------------------------------------------------------------------------
|
||||
# Theme files are NOT executed as shell scripts. load_theme() parses them
|
||||
# line by line. Only the following value forms are accepted:
|
||||
#
|
||||
# KEY="$ColorVarName" — reference to a colour variable from disp.sh
|
||||
# KEY="${ColorVarName}" — same with braces
|
||||
# KEY="\e[...m" — raw ANSI escape sequence (single block)
|
||||
#
|
||||
# Accepted keys:
|
||||
# PROMPT_COLOR_* — prompt slot colours (see profile.conf [prompt])
|
||||
# Standard colour vars — Black, Blue, On_IBlack, … (overrides the palette
|
||||
# from disp.sh for the whole shell session)
|
||||
#
|
||||
# Any unknown key, unsafe value, or shell construct will be ignored with a
|
||||
# warning — theme files cannot execute code.
|
||||
# ------------------------------------------------------------------------------
|
||||
# Inspired by the VS Code "Abyss" theme: deep navy/black backdrop, electric
|
||||
# teal highlights, golden-yellow accents, cool electric blue for identifiers.
|
||||
# Palette overrides shift the cooler hues to their high-intensity (electric)
|
||||
# equivalents — blue becomes IBlue, green shifts toward teal (ICyan).
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
# Palette overrides — shift colours toward cool electric hues
|
||||
Blue="\e[0;94m" # electric blue (IBlue — abyss identifier colour)
|
||||
Green="\e[0;96m" # teal (ICyan — abyss string colour)
|
||||
Yellow="\e[0;93m" # bright gold (IYellow — abyss constant colour)
|
||||
|
||||
PROMPT_COLOR_TIME_FG="$ICyan" # electric teal clock
|
||||
PROMPT_COLOR_TIME_BG="\e[48;2;0;60;70m" # very dark cyan bg for time
|
||||
PROMPT_COLOR_BAR_BG="\e[48;2;0;30;70m" # deep navy bar (24-bit)
|
||||
|
||||
PROMPT_COLOR_OK_FG="$ICyan" # teal on success
|
||||
PROMPT_COLOR_OK_MARK="$IGreen" # bright teal-green checkmark
|
||||
|
||||
PROMPT_COLOR_ERR_BG="\e[48;2;180;20;20m" # vivid crimson background (24-bit)
|
||||
PROMPT_COLOR_ERR_FG="\e[1;97m" # bold bright white — maximum contrast
|
||||
PROMPT_COLOR_ERR_MARK="$IYellow" # golden X
|
||||
|
||||
PROMPT_COLOR_ROOT_FG="$IRed" # red for root
|
||||
PROMPT_COLOR_USER_FG="$IBlue" # electric blue for user
|
||||
PROMPT_COLOR_DIR_FG="$ICyan" # teal path
|
||||
PROMPT_COLOR_CTX_FG="$IYellow" # context segment (git/conda)
|
||||
@@ -0,0 +1,43 @@
|
||||
# Adwaita prompt theme — clean GNOME defaults, blue accent, green/red status
|
||||
# ------------------------------------------------------------------------------
|
||||
# Theme files are NOT executed as shell scripts. load_theme() parses them
|
||||
# line by line. Only the following value forms are accepted:
|
||||
#
|
||||
# KEY="$ColorVarName" — reference to a colour variable from disp.sh
|
||||
# KEY="${ColorVarName}" — same with braces
|
||||
# KEY="\e[...m" — raw ANSI escape sequence (single block)
|
||||
#
|
||||
# Accepted keys:
|
||||
# PROMPT_COLOR_* — prompt slot colours (see profile.conf [prompt])
|
||||
# Standard colour vars — Black, Blue, On_IBlack, … (overrides the palette
|
||||
# from disp.sh for the whole shell session)
|
||||
#
|
||||
# Any unknown key, unsafe value, or shell construct will be ignored with a
|
||||
# warning — theme files cannot execute code.
|
||||
# ------------------------------------------------------------------------------
|
||||
# Follows the GNOME HIG colour intent: a single calm blue accent (#3584e4),
|
||||
# pleasant green for success (#33d17a), clear red for errors (#e01b24).
|
||||
# No intense saturation, no heavy remapping — legibility over spectacle.
|
||||
# Palette overrides gently shift Blue and Green to their IBlue/IGreen
|
||||
# variants to better match Adwaita's brighter, flatter tones.
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
# Palette overrides — align to Adwaita's brighter accent tones
|
||||
Blue="\e[0;94m" # IBlue → closer to Adwaita blue (#3584e4)
|
||||
Green="\e[0;92m" # IGreen → closer to Adwaita green (#33d17a)
|
||||
|
||||
PROMPT_COLOR_TIME_FG="$Cyan" # calm cyan clock text
|
||||
PROMPT_COLOR_TIME_BG="$On_IBlack" # dark grey clock background
|
||||
PROMPT_COLOR_BAR_BG="$On_Blue" # blue main bar (Adwaita accent)
|
||||
|
||||
PROMPT_COLOR_OK_FG="$White" # clean white on success
|
||||
PROMPT_COLOR_OK_MARK="$Green" # Adwaita green checkmark
|
||||
|
||||
PROMPT_COLOR_ERR_BG="$On_Red" # Adwaita red on failure
|
||||
PROMPT_COLOR_ERR_FG="$BIWhite" # bold bright white for maximum legibility
|
||||
PROMPT_COLOR_ERR_MARK="$Yellow" # yellow X (warning intent)
|
||||
|
||||
PROMPT_COLOR_ROOT_FG="$Red" # Adwaita red for root
|
||||
PROMPT_COLOR_USER_FG="$BBlue" # darker bold blue — readable on blue bar
|
||||
PROMPT_COLOR_DIR_FG="$IGreen" # Adwaita green for path
|
||||
PROMPT_COLOR_CTX_FG="$BIWhite" # context segment (git/conda)
|
||||
@@ -0,0 +1,33 @@
|
||||
# Dark prompt theme — dark grey bar, cyan user, magenta path
|
||||
# ------------------------------------------------------------------------------
|
||||
# Theme files are NOT executed as shell scripts. load_theme() parses them
|
||||
# line by line. Only the following value forms are accepted:
|
||||
#
|
||||
# KEY="$ColorVarName" — reference to a colour variable from disp.sh
|
||||
# KEY="${ColorVarName}" — same with braces
|
||||
# KEY="\e[...m" — raw ANSI escape sequence (single block)
|
||||
#
|
||||
# Accepted keys:
|
||||
# PROMPT_COLOR_* — prompt slot colours (see profile.conf [prompt])
|
||||
# Standard colour vars — Black, Blue, On_IBlack, … (overrides the palette
|
||||
# from disp.sh for the whole shell session)
|
||||
#
|
||||
# Any unknown key, unsafe value, or shell construct will be ignored with a
|
||||
# warning — theme files cannot execute code.
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
PROMPT_COLOR_TIME_FG="$ICyan" # Clock text
|
||||
PROMPT_COLOR_TIME_BG="$On_IBlack" # Clock background (black)
|
||||
PROMPT_COLOR_BAR_BG="$On_Black" # Main bar background (dark grey)
|
||||
|
||||
PROMPT_COLOR_OK_FG="$IGreen" # Exit-code text on success
|
||||
PROMPT_COLOR_OK_MARK="$BGreen" # Checkmark colour on success
|
||||
|
||||
PROMPT_COLOR_ERR_BG="$On_Red" # Exit-code background on failure
|
||||
PROMPT_COLOR_ERR_FG="$BIWhite" # Exit-code text on failure
|
||||
PROMPT_COLOR_ERR_MARK="$BIYellow" # X mark colour on failure
|
||||
|
||||
PROMPT_COLOR_ROOT_FG="$BIRed" # Username colour when root
|
||||
PROMPT_COLOR_USER_FG="$ICyan" # Username@host colour for normal users
|
||||
PROMPT_COLOR_DIR_FG="$IPurple" # Working directory colour
|
||||
PROMPT_COLOR_CTX_FG="$BIYellow" # Context segment (git/conda)
|
||||
@@ -0,0 +1,33 @@
|
||||
# Default prompt theme — blue bar, green user, cyan path
|
||||
# ------------------------------------------------------------------------------
|
||||
# Theme files are NOT executed as shell scripts. load_theme() parses them
|
||||
# line by line. Only the following value forms are accepted:
|
||||
#
|
||||
# KEY="$ColorVarName" — reference to a colour variable from disp.sh
|
||||
# KEY="${ColorVarName}" — same with braces
|
||||
# KEY="\e[...m" — raw ANSI escape sequence (single block)
|
||||
#
|
||||
# Accepted keys:
|
||||
# PROMPT_COLOR_* — prompt slot colours (see profile.conf [prompt])
|
||||
# Standard colour vars — Black, Blue, On_IBlack, … (overrides the palette
|
||||
# from disp.sh for the whole shell session)
|
||||
#
|
||||
# Any unknown key, unsafe value, or shell construct will be ignored with a
|
||||
# warning — theme files cannot execute code.
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
PROMPT_COLOR_TIME_FG="$Blue" # Clock text
|
||||
PROMPT_COLOR_TIME_BG="$On_White" # Clock background (dark grey)
|
||||
PROMPT_COLOR_BAR_BG="$On_Blue" # Main bar background
|
||||
|
||||
PROMPT_COLOR_OK_FG="$White" # Exit-code text on success
|
||||
PROMPT_COLOR_OK_MARK="$Green" # Checkmark colour on success
|
||||
|
||||
PROMPT_COLOR_ERR_BG="$On_Red" # Exit-code background on failure
|
||||
PROMPT_COLOR_ERR_FG="$White" # Exit-code text on failure
|
||||
PROMPT_COLOR_ERR_MARK="$BYellow" # X mark colour on failure
|
||||
|
||||
PROMPT_COLOR_ROOT_FG="$Red" # Username colour when root
|
||||
PROMPT_COLOR_USER_FG="$Green" # Username@host colour for normal users
|
||||
PROMPT_COLOR_DIR_FG="$ICyan" # Working directory colour
|
||||
PROMPT_COLOR_CTX_FG="$BIYellow" # Context segment (git/conda)
|
||||
@@ -0,0 +1,36 @@
|
||||
# Light prompt theme — white bar, blue user, purple path
|
||||
# ------------------------------------------------------------------------------
|
||||
# Theme files are NOT executed as shell scripts. load_theme() parses them
|
||||
# line by line. Only the following value forms are accepted:
|
||||
#
|
||||
# KEY="$ColorVarName" — reference to a colour variable from disp.sh
|
||||
# KEY="${ColorVarName}" — same with braces
|
||||
# KEY="\e[...m" — raw ANSI escape sequence (single block)
|
||||
#
|
||||
# Accepted keys:
|
||||
# PROMPT_COLOR_* — prompt slot colours (see profile.conf [prompt])
|
||||
# Standard colour vars — Black, Blue, On_IBlack, … (overrides the palette
|
||||
# from disp.sh for the whole shell session)
|
||||
#
|
||||
# Any unknown key, unsafe value, or shell construct will be ignored with a
|
||||
# warning — theme files cannot execute code.
|
||||
# ------------------------------------------------------------------------------
|
||||
# Opposite of dark.theme: backgrounds flip to bright whites, foregrounds
|
||||
# shift to their dark/regular equivalents for contrast on a light terminal.
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
PROMPT_COLOR_TIME_FG="$BBlack" # Clock text (bold black — forces true black on terminals that render Black as dark grey)
|
||||
PROMPT_COLOR_TIME_BG="$On_IWhite" # Clock background (On_Black → On_IWhite)
|
||||
PROMPT_COLOR_BAR_BG="$On_White" # Main bar background (On_IBlack → On_White)
|
||||
|
||||
PROMPT_COLOR_OK_FG="$Green" # Exit-code text on success (IGreen → Green)
|
||||
PROMPT_COLOR_OK_MARK="$BGreen" # Checkmark colour on success (unchanged — bold green reads on both)
|
||||
|
||||
PROMPT_COLOR_ERR_BG="$On_Red" # Exit-code background on failure (unchanged)
|
||||
PROMPT_COLOR_ERR_FG="$BIWhite" # Exit-code text on failure (unchanged — white on red works on both)
|
||||
PROMPT_COLOR_ERR_MARK="$BYellow" # X mark on failure (BIYellow → BYellow, less glaring on light)
|
||||
|
||||
PROMPT_COLOR_ROOT_FG="$Red" # Username when root (BIRed → Red)
|
||||
PROMPT_COLOR_USER_FG="$Blue" # Username@host normal user (ICyan → Blue)
|
||||
PROMPT_COLOR_DIR_FG="$Purple" # Working directory (IPurple → Purple)
|
||||
PROMPT_COLOR_CTX_FG="$BBlack" # Context segment (git/conda)
|
||||
@@ -0,0 +1,65 @@
|
||||
# Monochrome prompt theme — strict greyscale, no hue at all
|
||||
# ------------------------------------------------------------------------------
|
||||
# Theme files are NOT executed as shell scripts. load_theme() parses them
|
||||
# line by line. Only the following value forms are accepted:
|
||||
#
|
||||
# KEY="$ColorVarName" — reference to a colour variable from disp.sh
|
||||
# KEY="${ColorVarName}" — same with braces
|
||||
# KEY="\e[...m" — raw ANSI escape sequence (single block)
|
||||
#
|
||||
# Accepted keys:
|
||||
# PROMPT_COLOR_* — prompt slot colours (see profile.conf [prompt])
|
||||
# Standard colour vars — Black, Blue, On_IBlack, … (overrides the palette
|
||||
# from disp.sh for the whole shell session)
|
||||
#
|
||||
# Any unknown key, unsafe value, or shell construct will be ignored with a
|
||||
# warning — theme files cannot execute code.
|
||||
# ------------------------------------------------------------------------------
|
||||
# All hues are silenced — colour variables are remapped to greyscale ANSI
|
||||
# codes so that ls, man, grep, etc. also lose their colour cues. Contrast
|
||||
# is achieved entirely through brightness: dark grey / light grey / white.
|
||||
# Error state uses an inverted (white background, black text) bar so it
|
||||
# remains visually distinct without relying on red.
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
# Palette overrides — replace every hue with a grey equivalent
|
||||
Red="\e[0;37m" # light grey
|
||||
Green="\e[0;97m" # bright white (success intent kept as brightest)
|
||||
Yellow="\e[0;90m" # dark grey
|
||||
Blue="\e[0;90m" # dark grey
|
||||
Purple="\e[0;37m" # light grey
|
||||
Cyan="\e[0;37m" # light grey
|
||||
BRed="\e[1;37m" # bold light grey
|
||||
BGreen="\e[1;97m" # bold bright white
|
||||
BYellow="\e[1;90m" # bold dark grey
|
||||
BBlue="\e[1;90m" # bold dark grey
|
||||
BPurple="\e[1;37m" # bold light grey
|
||||
BCyan="\e[1;37m" # bold light grey
|
||||
IRed="\e[0;97m" # bright white
|
||||
IGreen="\e[0;97m" # bright white
|
||||
IYellow="\e[0;90m" # dark grey
|
||||
IBlue="\e[0;90m" # dark grey
|
||||
IPurple="\e[0;37m" # light grey
|
||||
ICyan="\e[0;37m" # light grey
|
||||
BIRed="\e[1;97m" # bold bright white
|
||||
BIGreen="\e[1;97m" # bold bright white
|
||||
BIYellow="\e[1;90m" # bold dark grey
|
||||
BIBlue="\e[1;90m" # bold dark grey
|
||||
BIPurple="\e[1;37m" # bold light grey
|
||||
BICyan="\e[1;37m" # bold light grey
|
||||
|
||||
PROMPT_COLOR_TIME_FG="$IBlack" # dark grey clock text (subtle)
|
||||
PROMPT_COLOR_TIME_BG="$On_IBlack" # dark grey clock background
|
||||
PROMPT_COLOR_BAR_BG="$On_IBlack" # dark grey main bar
|
||||
|
||||
PROMPT_COLOR_OK_FG="$IWhite" # bright white on success
|
||||
PROMPT_COLOR_OK_MARK="$BIWhite" # bold bright white checkmark
|
||||
|
||||
PROMPT_COLOR_ERR_BG="$On_White" # inverted: bright white bar on error
|
||||
PROMPT_COLOR_ERR_FG="$Black" # black text on white background
|
||||
PROMPT_COLOR_ERR_MARK="$BBlack" # bold black X
|
||||
|
||||
PROMPT_COLOR_ROOT_FG="$BIWhite" # bold bright white for root warning
|
||||
PROMPT_COLOR_USER_FG="$IWhite" # bright white for normal user
|
||||
PROMPT_COLOR_DIR_FG="$White" # standard white for path
|
||||
PROMPT_COLOR_CTX_FG="$BIWhite" # context segment (git/conda)
|
||||
@@ -0,0 +1,46 @@
|
||||
# Monokai prompt theme — high-saturation, vivid hues on near-black
|
||||
# ------------------------------------------------------------------------------
|
||||
# Theme files are NOT executed as shell scripts. load_theme() parses them
|
||||
# line by line. Only the following value forms are accepted:
|
||||
#
|
||||
# KEY="$ColorVarName" — reference to a colour variable from disp.sh
|
||||
# KEY="${ColorVarName}" — same with braces
|
||||
# KEY="\e[...m" — raw ANSI escape sequence (single block)
|
||||
#
|
||||
# Accepted keys:
|
||||
# PROMPT_COLOR_* — prompt slot colours (see profile.conf [prompt])
|
||||
# Standard colour vars — Black, Blue, On_IBlack, … (overrides the palette
|
||||
# from disp.sh for the whole shell session)
|
||||
#
|
||||
# Any unknown key, unsafe value, or shell construct will be ignored with a
|
||||
# warning — theme files cannot execute code.
|
||||
# ------------------------------------------------------------------------------
|
||||
# Monokai's signature: lime green, orange-yellow, hot pink/red, bright violet,
|
||||
# electric cyan — all on a near-black (#272822) background.
|
||||
# Palette overrides remap the dim ANSI regulars to their vivid high-intensity
|
||||
# equivalents so that ls, grep colour output, etc. also look more "Monokai".
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
# Palette overrides — boost regular colours to Monokai-vivid equivalents
|
||||
Red="\e[0;91m" # hot pink/red (#F92672)
|
||||
Green="\e[0;92m" # lime green (#A6E22E)
|
||||
Yellow="\e[0;93m" # orange-yellow (#E6DB74 / #FD971F)
|
||||
Blue="\e[0;94m" # electric blue (#66D9E8 → shift blue)
|
||||
Purple="\e[0;95m" # bright violet (#AE81FF)
|
||||
Cyan="\e[0;96m" # electric cyan (#66D9E8)
|
||||
|
||||
PROMPT_COLOR_TIME_FG="$IYellow" # orange clock text
|
||||
PROMPT_COLOR_TIME_BG="$On_IBlack" # near-black Monokai background
|
||||
PROMPT_COLOR_BAR_BG="$On_IBlack" # flat dark bar
|
||||
|
||||
PROMPT_COLOR_OK_FG="$IGreen" # lime green on success
|
||||
PROMPT_COLOR_OK_MARK="$IGreen" # lime green checkmark
|
||||
|
||||
PROMPT_COLOR_ERR_BG="$On_IBlack" # keep dark — use colour for contrast
|
||||
PROMPT_COLOR_ERR_FG="$IRed" # hot pink on failure
|
||||
PROMPT_COLOR_ERR_MARK="$IRed" # hot pink X
|
||||
|
||||
PROMPT_COLOR_ROOT_FG="$IRed" # hot pink for root
|
||||
PROMPT_COLOR_USER_FG="$IYellow" # orange-yellow for user
|
||||
PROMPT_COLOR_DIR_FG="$ICyan" # electric cyan for path
|
||||
PROMPT_COLOR_CTX_FG="$IYellow" # context segment (git/conda)
|
||||
@@ -0,0 +1,43 @@
|
||||
# Plasma prompt theme — vivid KDE Plasma purples and electric cyan
|
||||
# ------------------------------------------------------------------------------
|
||||
# Theme files are NOT executed as shell scripts. load_theme() parses them
|
||||
# line by line. Only the following value forms are accepted:
|
||||
#
|
||||
# KEY="$ColorVarName" — reference to a colour variable from disp.sh
|
||||
# KEY="${ColorVarName}" — same with braces
|
||||
# KEY="\e[...m" — raw ANSI escape sequence (single block)
|
||||
#
|
||||
# Accepted keys:
|
||||
# PROMPT_COLOR_* — prompt slot colours (see profile.conf [prompt])
|
||||
# Standard colour vars — Black, Blue, On_IBlack, … (overrides the palette
|
||||
# from disp.sh for the whole shell session)
|
||||
#
|
||||
# Any unknown key, unsafe value, or shell construct will be ignored with a
|
||||
# warning — theme files cannot execute code.
|
||||
# ------------------------------------------------------------------------------
|
||||
# Captures the charged, vivid energy of KDE Plasma: dominant bright magenta/
|
||||
# purple, electric cyan highlights, all on a dark background. Palette overrides
|
||||
# boost Blue, Purple, and Cyan to their high-intensity equivalents so that
|
||||
# shell colour output reflects the same vivid palette.
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
# Palette overrides — vivid charged hues
|
||||
Blue="\e[0;94m" # electric blue (IBlue)
|
||||
Purple="\e[0;95m" # vivid magenta (IPurple — Plasma's signature colour)
|
||||
Cyan="\e[0;96m" # electric cyan (ICyan)
|
||||
|
||||
PROMPT_COLOR_TIME_FG="$BIPurple" # vivid purple clock text
|
||||
PROMPT_COLOR_TIME_BG="\e[48;2;50;50;55m" # deep charcoal (darker than On_IBlack)
|
||||
PROMPT_COLOR_BAR_BG="\e[48;2;75;0;130m" # deep indigo-purple (24-bit) — darker than On_Purple
|
||||
|
||||
PROMPT_COLOR_OK_FG="$ICyan" # electric cyan on success
|
||||
PROMPT_COLOR_OK_MARK="$IGreen" # green checkmark
|
||||
|
||||
PROMPT_COLOR_ERR_BG="$On_Red" # red bar on failure
|
||||
PROMPT_COLOR_ERR_FG="$IWhite" # bright white text
|
||||
PROMPT_COLOR_ERR_MARK="$IYellow" # yellow X
|
||||
|
||||
PROMPT_COLOR_ROOT_FG="$IRed" # red for root
|
||||
PROMPT_COLOR_USER_FG="$BIPurple" # bold vivid purple for user
|
||||
PROMPT_COLOR_DIR_FG="$ICyan" # electric cyan path
|
||||
PROMPT_COLOR_CTX_FG="$IYellow" # context segment (git/conda)
|
||||
@@ -0,0 +1,128 @@
|
||||
# Solarized Light prompt theme — exact 24-bit / true-colour palette
|
||||
# ------------------------------------------------------------------------------
|
||||
# Theme files are NOT executed as shell scripts. load_theme() parses them
|
||||
# line by line. Only the following value forms are accepted:
|
||||
#
|
||||
# KEY="$ColorVarName" — reference to a colour variable from disp.sh
|
||||
# KEY="${ColorVarName}" — same with braces
|
||||
# KEY="\e[...m" — raw ANSI escape sequence (single block)
|
||||
#
|
||||
# Accepted keys:
|
||||
# PROMPT_COLOR_* — prompt slot colours (see profile.conf [prompt])
|
||||
# Standard colour vars — Black, Blue, On_IBlack, … (overrides the palette
|
||||
# from disp.sh for the whole shell session)
|
||||
#
|
||||
# Any unknown key, unsafe value, or shell construct will be ignored with a
|
||||
# warning — theme files cannot execute code.
|
||||
# ------------------------------------------------------------------------------
|
||||
# Requires a terminal with true-colour / 24-bit support.
|
||||
# Check with: printf '\e[38;2;220;50;47mred\e[0m\n'
|
||||
# If you see solid red text, your terminal supports this theme.
|
||||
# (konsole, iTerm2, kitty, alacritty, Windows Terminal all do)
|
||||
#
|
||||
# Solarized Light base tones (inverted vs Dark):
|
||||
# Base3 #fdf6e3 →253 246 227 (main background — lightest)
|
||||
# Base2 #eee8d5 →238 232 213 (background highlights)
|
||||
# Base1 #93a1a1 →147 161 161 (comments / secondary content)
|
||||
# Base0 #839496 →131 148 150 (body text — secondary)
|
||||
# Base00 #657b83 →101 123 131 (body text — main on light bg)
|
||||
# Base01 #586e75 → 88 110 117 (emphasis)
|
||||
# Base02 #073642 → 7 54 66 (darkest — used for strong contrast)
|
||||
#
|
||||
# Accent colours are identical to Solarized Dark:
|
||||
# Yellow #b58900 →181 137 0
|
||||
# Orange #cb4b16 →203 75 22
|
||||
# Red #dc322f →220 50 47
|
||||
# Magenta #d33682 →211 54 130
|
||||
# Violet #6c71c4 →108 113 196
|
||||
# Blue #268bd2 → 38 139 210
|
||||
# Cyan #2aa198 → 42 161 152
|
||||
# Green #859900 →133 153 0
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
# ---- Foreground palette overrides -------------------------------------------
|
||||
# The accent fg colours are identical to Dark — only the base/neutral roles flip.
|
||||
# Regular
|
||||
Black="\e[38;2;253;246;227m" # Base3 — lightest (fg on light bg = invisible; used as bg fg pair complement)
|
||||
Red="\e[38;2;220;50;47m" # Red
|
||||
Green="\e[38;2;133;153;0m" # Green
|
||||
Yellow="\e[38;2;181;137;0m" # Yellow — primary accent
|
||||
Blue="\e[38;2;38;139;210m" # Blue
|
||||
Purple="\e[38;2;211;54;130m" # Magenta
|
||||
Cyan="\e[38;2;42;161;152m" # Cyan
|
||||
White="\e[38;2;101;123;131m" # Base00 — main body text on light bg
|
||||
|
||||
# Bold
|
||||
BBlack="\e[1;38;2;238;232;213m" # Base2 bold
|
||||
BRed="\e[1;38;2;220;50;47m" # Red bold
|
||||
BGreen="\e[1;38;2;133;153;0m" # Green bold
|
||||
BYellow="\e[1;38;2;181;137;0m" # Yellow bold
|
||||
BBlue="\e[1;38;2;38;139;210m" # Blue bold
|
||||
BPurple="\e[1;38;2;211;54;130m" # Magenta bold
|
||||
BCyan="\e[1;38;2;42;161;152m" # Cyan bold
|
||||
BWhite="\e[1;38;2;88;110;117m" # Base01 bold — emphasis text
|
||||
|
||||
# High intensity (emphasis / I* family)
|
||||
IBlack="\e[38;2;147;161;161m" # Base1 — secondary/comments
|
||||
IRed="\e[38;2;203;75;22m" # Orange — Solarized's "bright red"
|
||||
IGreen="\e[38;2;133;153;0m" # Green (no brighter variant)
|
||||
IYellow="\e[38;2;181;137;0m" # Yellow (no brighter variant)
|
||||
IBlue="\e[38;2;108;113;196m" # Violet — Solarized's "bright blue"
|
||||
IPurple="\e[38;2;211;54;130m" # Magenta (no brighter variant)
|
||||
ICyan="\e[38;2;42;161;152m" # Cyan (no brighter variant)
|
||||
IWhite="\e[38;2;88;110;117m" # Base01 — emphasis
|
||||
|
||||
# Bold high intensity
|
||||
BIBlack="\e[1;38;2;147;161;161m" # Base1 bold
|
||||
BIRed="\e[1;38;2;203;75;22m" # Orange bold
|
||||
BIGreen="\e[1;38;2;133;153;0m" # Green bold
|
||||
BIYellow="\e[1;38;2;181;137;0m" # Yellow bold
|
||||
BIBlue="\e[1;38;2;108;113;196m" # Violet bold
|
||||
BIPurple="\e[1;38;2;211;54;130m" # Magenta bold
|
||||
BICyan="\e[1;38;2;42;161;152m" # Cyan bold
|
||||
BIWhite="\e[1;38;2;88;110;117m" # Base01 bold
|
||||
|
||||
# ---- Background palette overrides -------------------------------------------
|
||||
# Light bases flip: On_Black → Base3 (lightest), On_IBlack → Base2 (highlights)
|
||||
On_Black="\e[48;2;253;246;227m" # Base3 — main light background
|
||||
On_Red="\e[48;2;220;50;47m" # Red
|
||||
On_Green="\e[48;2;133;153;0m" # Green
|
||||
On_Yellow="\e[48;2;181;137;0m" # Yellow
|
||||
On_Blue="\e[48;2;38;139;210m" # Blue
|
||||
On_Purple="\e[48;2;211;54;130m" # Magenta
|
||||
On_Cyan="\e[48;2;42;161;152m" # Cyan
|
||||
On_White="\e[48;2;101;123;131m" # Base00
|
||||
|
||||
On_IBlack="\e[48;2;238;232;213m" # Base2 — background highlights (slightly darker than Base3)
|
||||
On_IRed="\e[48;2;203;75;22m" # Orange
|
||||
On_IGreen="\e[48;2;133;153;0m" # Green
|
||||
On_IYellow="\e[48;2;181;137;0m" # Yellow
|
||||
On_IBlue="\e[48;2;108;113;196m" # Violet
|
||||
On_IPurple="\e[48;2;211;54;130m" # Magenta
|
||||
On_ICyan="\e[48;2;42;161;152m" # Cyan
|
||||
On_IWhite="\e[48;2;88;110;117m" # Base01
|
||||
|
||||
# ---- Special codes ----------------------------------------------------------
|
||||
DEFAULTFG="\e[38;2;101;123;131m" # Base00 — default foreground on light bg
|
||||
DEFAULTBG="\e[48;2;253;246;227m" # Base3 — default background
|
||||
RESETCOL="\e[0m"
|
||||
|
||||
# ---- Prompt colour slots ----------------------------------------------------
|
||||
# All specified as direct ANSI sequences to avoid ordering dependencies
|
||||
# with the palette overrides above.
|
||||
|
||||
PROMPT_COLOR_TIME_FG="\e[38;2;181;137;0m" # Yellow — primary accent
|
||||
PROMPT_COLOR_TIME_BG="\e[48;2;238;232;213m" # Base2 — slightly darker than bg
|
||||
PROMPT_COLOR_BAR_BG="\e[48;2;238;232;213m" # Base2 — warm light bar
|
||||
|
||||
PROMPT_COLOR_OK_FG="\e[38;2;7;54;66m" # Base02 — dark text for contrast on light
|
||||
PROMPT_COLOR_OK_MARK="\e[38;2;133;153;0m" # Green — checkmark
|
||||
|
||||
PROMPT_COLOR_ERR_BG="\e[48;2;220;50;47m" # Red — error background
|
||||
PROMPT_COLOR_ERR_FG="\e[38;2;253;246;227m" # Base3 — light text on red
|
||||
PROMPT_COLOR_ERR_MARK="\e[38;2;253;246;227m" # Base3 — X mark (bright on red)
|
||||
|
||||
PROMPT_COLOR_ROOT_FG="\e[38;2;220;50;47m" # Red — root warning
|
||||
PROMPT_COLOR_USER_FG="\e[38;2;42;161;152m" # Cyan — normal user
|
||||
PROMPT_COLOR_DIR_FG="\e[38;2;38;139;210m" # Blue — working directory
|
||||
PROMPT_COLOR_CTX_FG="\e[38;2;181;137;0m" # Yellow — context segment (git/conda)
|
||||
@@ -0,0 +1,123 @@
|
||||
# Solarized Dark prompt theme — exact 24-bit / true-colour palette
|
||||
# ------------------------------------------------------------------------------
|
||||
# Theme files are NOT executed as shell scripts. load_theme() parses them
|
||||
# line by line. Only the following value forms are accepted:
|
||||
#
|
||||
# KEY="$ColorVarName" — reference to a colour variable from disp.sh
|
||||
# KEY="${ColorVarName}" — same with braces
|
||||
# KEY="\e[...m" — raw ANSI escape sequence (single block)
|
||||
#
|
||||
# Accepted keys:
|
||||
# PROMPT_COLOR_* — prompt slot colours (see profile.conf [prompt])
|
||||
# Standard colour vars — Black, Blue, On_IBlack, … (overrides the palette
|
||||
# from disp.sh for the whole shell session)
|
||||
#
|
||||
# Any unknown key, unsafe value, or shell construct will be ignored with a
|
||||
# warning — theme files cannot execute code.
|
||||
# ------------------------------------------------------------------------------
|
||||
# Requires a terminal with true-colour / 24-bit support.
|
||||
# Check with: printf '\e[38;2;220;50;47mred\e[0m\n'
|
||||
# If you see solid red text, your terminal supports this theme.
|
||||
# (konsole, iTerm2, kitty, alacritty, Windows Terminal all do)
|
||||
#
|
||||
# Solarized Dark exact hex → RGB mapping used below:
|
||||
# Base03 #002b36 → 0 43 54 (darkest background)
|
||||
# Base02 #073642 → 7 54 66 (background highlights)
|
||||
# Base01 #586e75 → 88 110 117 (comments / secondary content)
|
||||
# Base00 #657b83 →101 123 131 (body text — dark background)
|
||||
# Base0 #839496 →131 148 150 (body text — main)
|
||||
# Base1 #93a1a1 →147 161 161 (optional emphasis)
|
||||
# Yellow #b58900 →181 137 0
|
||||
# Orange #cb4b16 →203 75 22
|
||||
# Red #dc322f →220 50 47
|
||||
# Magenta #d33682 →211 54 130
|
||||
# Violet #6c71c4 →108 113 196
|
||||
# Blue #268bd2 → 38 139 210
|
||||
# Cyan #2aa198 → 42 161 152
|
||||
# Green #859900 →133 153 0
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
# ---- Foreground palette overrides -------------------------------------------
|
||||
# Regular
|
||||
Black="\e[38;2;0;43;54m" # Base03 — darkest content fg
|
||||
Red="\e[38;2;220;50;47m" # Red
|
||||
Green="\e[38;2;133;153;0m" # Green
|
||||
Yellow="\e[38;2;181;137;0m" # Yellow — primary accent
|
||||
Blue="\e[38;2;38;139;210m" # Blue
|
||||
Purple="\e[38;2;211;54;130m" # Magenta
|
||||
Cyan="\e[38;2;42;161;152m" # Cyan
|
||||
White="\e[38;2;131;148;150m" # Base0 — body text
|
||||
|
||||
# Bold
|
||||
BBlack="\e[1;38;2;7;54;66m" # Base02 bold
|
||||
BRed="\e[1;38;2;220;50;47m" # Red bold
|
||||
BGreen="\e[1;38;2;133;153;0m" # Green bold
|
||||
BYellow="\e[1;38;2;181;137;0m" # Yellow bold
|
||||
BBlue="\e[1;38;2;38;139;210m" # Blue bold
|
||||
BPurple="\e[1;38;2;211;54;130m" # Magenta bold
|
||||
BCyan="\e[1;38;2;42;161;152m" # Cyan bold
|
||||
BWhite="\e[1;38;2;147;161;161m" # Base1 bold
|
||||
|
||||
# High intensity (brighter / emphasis roles in Solarized)
|
||||
IBlack="\e[38;2;88;110;117m" # Base01 — secondary/comments
|
||||
IRed="\e[38;2;203;75;22m" # Orange — Solarized's "bright red"
|
||||
IGreen="\e[38;2;133;153;0m" # Green (no brighter variant)
|
||||
IYellow="\e[38;2;181;137;0m" # Yellow (no brighter variant)
|
||||
IBlue="\e[38;2;108;113;196m" # Violet — Solarized's "bright blue"
|
||||
IPurple="\e[38;2;211;54;130m" # Magenta (no brighter variant)
|
||||
ICyan="\e[38;2;42;161;152m" # Cyan (no brighter variant)
|
||||
IWhite="\e[38;2;147;161;161m" # Base1 — optional emphasis
|
||||
|
||||
# Bold high intensity
|
||||
BIBlack="\e[1;38;2;88;110;117m" # Base01 bold
|
||||
BIRed="\e[1;38;2;203;75;22m" # Orange bold
|
||||
BIGreen="\e[1;38;2;133;153;0m" # Green bold
|
||||
BIYellow="\e[1;38;2;181;137;0m" # Yellow bold
|
||||
BIBlue="\e[1;38;2;108;113;196m" # Violet bold
|
||||
BIPurple="\e[1;38;2;211;54;130m" # Magenta bold
|
||||
BICyan="\e[1;38;2;42;161;152m" # Cyan bold
|
||||
BIWhite="\e[1;38;2;147;161;161m" # Base1 bold
|
||||
|
||||
# ---- Background palette overrides -------------------------------------------
|
||||
On_Black="\e[48;2;0;43;54m" # Base03
|
||||
On_Red="\e[48;2;220;50;47m" # Red
|
||||
On_Green="\e[48;2;133;153;0m" # Green
|
||||
On_Yellow="\e[48;2;181;137;0m" # Yellow
|
||||
On_Blue="\e[48;2;38;139;210m" # Blue
|
||||
On_Purple="\e[48;2;211;54;130m" # Magenta
|
||||
On_Cyan="\e[48;2;42;161;152m" # Cyan
|
||||
On_White="\e[48;2;131;148;150m" # Base0
|
||||
|
||||
On_IBlack="\e[48;2;7;54;66m" # Base02 — background highlights
|
||||
On_IRed="\e[48;2;203;75;22m" # Orange
|
||||
On_IGreen="\e[48;2;133;153;0m" # Green
|
||||
On_IYellow="\e[48;2;181;137;0m" # Yellow
|
||||
On_IBlue="\e[48;2;108;113;196m" # Violet
|
||||
On_IPurple="\e[48;2;211;54;130m" # Magenta
|
||||
On_ICyan="\e[48;2;42;161;152m" # Cyan
|
||||
On_IWhite="\e[48;2;147;161;161m" # Base1
|
||||
|
||||
# ---- Special codes ----------------------------------------------------------
|
||||
DEFAULTFG="\e[38;2;131;148;150m" # Base0 — default foreground
|
||||
DEFAULTBG="\e[48;2;0;43;54m" # Base03 — default background
|
||||
RESETCOL="\e[0m"
|
||||
|
||||
# ---- Prompt colour slots ----------------------------------------------------
|
||||
# All specified as direct ANSI sequences to avoid ordering dependencies
|
||||
# with the palette overrides above.
|
||||
|
||||
PROMPT_COLOR_TIME_FG="\e[38;2;181;137;0m" # Yellow — primary accent
|
||||
PROMPT_COLOR_TIME_BG="\e[48;2;0;43;54m" # Base03 — darkest background
|
||||
PROMPT_COLOR_BAR_BG="\e[48;2;88;110;117m" # Base01 — slightly brighter bar (was Base02)
|
||||
|
||||
PROMPT_COLOR_OK_FG="\e[38;2;131;148;150m" # Base0 — body text on success
|
||||
PROMPT_COLOR_OK_MARK="\e[38;2;133;153;0m" # Green — checkmark
|
||||
|
||||
PROMPT_COLOR_ERR_BG="\e[48;2;180;20;15m" # deeper crimson — more contrast than Solarized Red
|
||||
PROMPT_COLOR_ERR_FG="\e[1;38;2;255;255;255m" # bold pure white — maximum contrast on dark red
|
||||
PROMPT_COLOR_ERR_MARK="\e[1;38;2;253;246;227m" # Base3 bold — bright warm mark stands out on crimson
|
||||
|
||||
PROMPT_COLOR_ROOT_FG="\e[38;2;220;50;47m" # Red — root warning
|
||||
PROMPT_COLOR_USER_FG="\e[38;2;42;161;152m" # Cyan — normal user
|
||||
PROMPT_COLOR_DIR_FG="\e[38;2;38;139;210m" # Blue — working directory
|
||||
PROMPT_COLOR_CTX_FG="\e[38;2;181;137;0m" # Yellow — context segment (git/conda)
|
||||
@@ -0,0 +1,492 @@
|
||||
#!/usr/bin/env bash
|
||||
# ------------------------------------------------------------------------------
|
||||
# Copyright (c) 2013-2026 Geoffray Levasseur <fatalerrors@geoffray-levasseur.org>
|
||||
# Protected by the BSD3 license. Please read bellow for details.
|
||||
#
|
||||
# * Redistribution and use in source and binary forms,
|
||||
# * with or without modification, are permitted provided
|
||||
# * that the following conditions are met:
|
||||
# *
|
||||
# * Redistributions of source code must retain the above
|
||||
# * copyright notice, this list of conditions and the
|
||||
# * following disclaimer.
|
||||
# *
|
||||
# * Redistributions in binary form must reproduce the above
|
||||
# * copyright notice, this list of conditions and the following
|
||||
# * disclaimer in the documentation and/or other materials
|
||||
# * provided with the distribution.
|
||||
# *
|
||||
# * Neither the name of the copyright holder nor the names
|
||||
# * of any other contributors may be used to endorse or
|
||||
# * promote products derived from this software without
|
||||
# * specific prior written permission.
|
||||
# *
|
||||
# * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
|
||||
# * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
|
||||
# * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
|
||||
# * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
# * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
|
||||
# * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
# * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
|
||||
# * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
# * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||
# * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
|
||||
# * OF SUCH DAMAGE.
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
export BASE_URL="https://git.geoffray-levasseur.org/fatalerrors/profile"
|
||||
|
||||
# Load [updates] configuration before building the URLs so that
|
||||
# UPDT_DEFAULT_BRANCH (if set in profile.conf) is already available.
|
||||
load_conf "updates"
|
||||
|
||||
# Common helpers, then the ones private to this module (helpers/updates.sh)
|
||||
# shellcheck source=../helpers/common.sh
|
||||
declare -F _profile_load_helpers >/dev/null || . "$(dirname -- "${BASH_SOURCE[0]}")/../helpers/common.sh"
|
||||
_profile_load_helpers "${BASH_SOURCE[0]}"
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
if ! _updt_valid_branch "${UPDT_DEFAULT_BRANCH:-master}"; then
|
||||
disp W "Invalid UPDT_DEFAULT_BRANCH '${UPDT_DEFAULT_BRANCH}', using master."
|
||||
UPDT_DEFAULT_BRANCH=master
|
||||
fi
|
||||
|
||||
export UPDT_DEFAULT_BRANCH="${UPDT_DEFAULT_BRANCH:-master}"
|
||||
export UPDT_URL="$BASE_URL/raw/branch/$UPDT_DEFAULT_BRANCH"
|
||||
export ARCH_URL="$BASE_URL/archive/$UPDT_DEFAULT_BRANCH.tar.gz"
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# Check whether a newer profile version is available
|
||||
# Usage: check_updates [-q]
|
||||
# If -q is specified, the function will operate in quiet mode (internal use
|
||||
# only): only the "update available" notice is displayed, errors are silent.
|
||||
# Return status: 0 up to date, 1 update available, 2 invalid option,
|
||||
# 4 temporary file error, 5 version unavailable or invalid.
|
||||
check_updates()
|
||||
{
|
||||
local quiet=0 result=5 PARSED
|
||||
local vfile="" lastver=""
|
||||
|
||||
PARSED=$(getopt -o hq --long help,quiet -n 'check_updates' -- "$@")
|
||||
# shellcheck disable=SC2181 # getopt return code is checked immediately after
|
||||
if [[ $? -ne 0 ]]; then
|
||||
disp E "Invalid options, use \"check_updates --help\" to display usage."
|
||||
return 2
|
||||
fi
|
||||
# Restore the getopt-normalised (shell-quoted) argument list
|
||||
local -a _pargs=()
|
||||
_getopt_split _pargs "$PARSED"
|
||||
set -- "${_pargs[@]}"
|
||||
|
||||
while true; do
|
||||
case "$1" in
|
||||
-h|--help)
|
||||
printf "check_updates: Check whether a newer profile version is available.\n\n"
|
||||
printf "Usage: check_updates [-q|--quiet]\n"
|
||||
printf "This command only checks availability; it does not modify the installation.\n"
|
||||
return 0
|
||||
;;
|
||||
-q|--quiet)
|
||||
quiet=1
|
||||
shift
|
||||
;;
|
||||
--)
|
||||
shift
|
||||
break
|
||||
;;
|
||||
*)
|
||||
break
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
(( quiet != 1 )) && disp I "Checking for updates..."
|
||||
|
||||
vfile=$(mktemp "${TMPDIR:-/tmp}/profile_version.XXXXXX") || {
|
||||
(( quiet != 1 )) && disp E "Failed to create a temporary file."
|
||||
return 4
|
||||
}
|
||||
|
||||
# In quiet mode (startup), use a short timeout so a missing or slow network
|
||||
# never blocks the interactive prompt.
|
||||
local dwl_opts=()
|
||||
(( quiet == 1 )) && dwl_opts+=(-t 3)
|
||||
|
||||
dwl "${dwl_opts[@]}" "$UPDT_URL/version" "$vfile" >/dev/null 2>&1 || {
|
||||
rm -f "$vfile"
|
||||
(( quiet != 1 )) && disp E "Cannot download version file; unable to continue."
|
||||
return 5
|
||||
}
|
||||
|
||||
# First line only, and only a plausible version: the text comes from the
|
||||
# network and is displayed by disp, which interprets backslash escapes (a
|
||||
# crafted file, or a captive portal page, could otherwise inject terminal
|
||||
# control sequences at every shell startup).
|
||||
IFS= read -r lastver < "$vfile" || true
|
||||
rm -f "$vfile"
|
||||
lastver="${lastver%$'\r'}"
|
||||
lastver="${lastver#"${lastver%%[![:space:]]*}"}"
|
||||
lastver="${lastver%"${lastver##*[![:space:]]}"}"
|
||||
if [[ ! "$lastver" =~ ^[0-9]+(\.[0-9]+)*([-_.+][0-9A-Za-z._+-]+)?$ ]]; then
|
||||
(( quiet != 1 )) && disp E "The downloaded version file is empty or invalid; unable to continue."
|
||||
return 5
|
||||
fi
|
||||
|
||||
if [[ "$lastver" == "${PROFVERSION:-}" ]]; then
|
||||
(( quiet != 1 )) && disp I "Your version is up-to-date."
|
||||
result=0
|
||||
elif _updt_is_newer "${PROFVERSION:-}" "$lastver"; then
|
||||
disp I "Installed: ${PROFVERSION:-unknown}. Available: $lastver."
|
||||
(( quiet != 1 )) && disp I "You should upgrade when possible."
|
||||
result=1
|
||||
else
|
||||
(( quiet != 1 )) && disp I "Installed version (${PROFVERSION:-}) is more recent than the published one ($lastver)."
|
||||
result=0
|
||||
fi
|
||||
|
||||
return $result
|
||||
}
|
||||
export -f check_updates
|
||||
# ------------------------------------------------------------------------------
|
||||
# Apply the available profile upgrade
|
||||
# Usage: profile_upgrade [options]
|
||||
# Return status: 0 done (or nothing to do), 1 refused/bad context, 2 git or
|
||||
# option error, 3 git/install error, 4 local archive or force error, 5
|
||||
# temporary directory, 6 download, 7 extraction, 8 archive content, 9 copy.
|
||||
profile_upgrade()
|
||||
{
|
||||
local PARSED
|
||||
local check_rc=0 dry_run=0 force_git=0 switch_to_git=0
|
||||
local archive_file="" tmpbase="" use_archive=0 branch=""
|
||||
local tmpdir="" archive="" extracted_root=""
|
||||
|
||||
PARSED=$(getopt -o hf:t:nFb:g --long help,file:,tmpdir:,dry-run,force,branch:,switch-to-git -n 'profile_upgrade' -- "$@")
|
||||
# shellcheck disable=SC2181 # getopt return code is checked immediately after
|
||||
if [[ $? -ne 0 ]]; then
|
||||
disp E "Invalid options, use \"profile_upgrade --help\" to display usage."
|
||||
return 2
|
||||
fi
|
||||
# Restore the getopt-normalised (shell-quoted) argument list
|
||||
local -a _pargs=()
|
||||
_getopt_split _pargs "$PARSED"
|
||||
set -- "${_pargs[@]}"
|
||||
while true; do
|
||||
case "$1" in
|
||||
-h|--help)
|
||||
printf "profile_upgrade: Apply the available profile upgrade.\n\n"
|
||||
printf "Usage: profile_upgrade [options]\n\n"
|
||||
printf "Options:\n"
|
||||
printf "\t-h, --help\t\tDisplay this help screen\n"
|
||||
printf "\t-f, --file ARCHIVE\tUse a local archive file for the upgrade\n"
|
||||
printf "\t-t, --tmpdir DIR\tCreate the temporary working directory under DIR\n"
|
||||
printf "\t-b, --branch NAME\tUse NAME as the target Git branch\n"
|
||||
printf "\t-g, --switch-to-git\tReplace current install with a fresh Git clone\n"
|
||||
printf "\t-n, --dry-run\t\tDisplay what would be done without changing anything\n"
|
||||
printf "\t-F, --force\t\tDiscard local changes before upgrading\n\n"
|
||||
printf "If the profile is installed from Git, the upgrade uses 'git pull --ff-only'.\n"
|
||||
printf "Otherwise, it downloads or applies an archive and refreshes the files.\n"
|
||||
return 0
|
||||
;;
|
||||
-f|--file)
|
||||
archive_file="$2"
|
||||
use_archive=1
|
||||
shift 2
|
||||
;;
|
||||
-t|--tmpdir)
|
||||
tmpbase="$2"
|
||||
shift 2
|
||||
;;
|
||||
-b|--branch)
|
||||
branch="$2"
|
||||
shift 2
|
||||
;;
|
||||
-g|--switch-to-git)
|
||||
switch_to_git=1
|
||||
shift
|
||||
;;
|
||||
-n|--dry-run)
|
||||
dry_run=1
|
||||
shift
|
||||
;;
|
||||
-F|--force)
|
||||
force_git=1
|
||||
shift
|
||||
;;
|
||||
--)
|
||||
shift
|
||||
break
|
||||
;;
|
||||
*)
|
||||
disp E "Invalid options, use \"profile_upgrade --help\" to display usage."
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
# Target branch: explicit -b flag > config default > master
|
||||
local target_branch="${branch:-${UPDT_DEFAULT_BRANCH:-master}}"
|
||||
if ! _updt_valid_branch "$target_branch"; then
|
||||
disp E "Invalid branch name '$target_branch'."
|
||||
return 1
|
||||
fi
|
||||
|
||||
if (( ! use_archive && ! switch_to_git )); then
|
||||
check_updates -q
|
||||
check_rc=$?
|
||||
if (( check_rc == 0 )); then
|
||||
disp I "No update available."
|
||||
return 0
|
||||
elif (( check_rc > 1 )); then
|
||||
disp E "Unable to check whether an update is available."
|
||||
return "$check_rc"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ ! -s $MYPATH/profile.sh ]]; then
|
||||
disp E "Install path detection failed; cannot upgrade automatically."
|
||||
return 1
|
||||
fi
|
||||
|
||||
if [[ -d $MYPATH/.git ]] && (( use_archive )) && (( ! force_git )); then
|
||||
disp E "Refusing archive upgrade on a Git install without --force."
|
||||
return 1
|
||||
fi
|
||||
|
||||
if (( switch_to_git )); then
|
||||
command -v git >/dev/null 2>&1 || {
|
||||
disp E "Git is required to switch this install to a Git clone."
|
||||
return 3
|
||||
}
|
||||
local backup_dir="${MYPATH}.pre-git.$$.bak"
|
||||
if (( dry_run )); then
|
||||
disp I "[dry-run] mv \"$MYPATH\" \"$backup_dir\""
|
||||
disp I "[dry-run] git clone $BASE_URL \"$MYPATH\""
|
||||
[[ -n "$branch" ]] && disp I "[dry-run] git -C \"$MYPATH\" checkout $branch"
|
||||
disp I "[dry-run] copy profile.conf and custom themes from \"$backup_dir\""
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [[ -d $MYPATH/.git ]]; then
|
||||
disp W "Git repository already present; no switch is needed."
|
||||
else
|
||||
mv "$MYPATH" "$backup_dir" || {
|
||||
disp E "Failed to move current install out of the way."
|
||||
return 3
|
||||
}
|
||||
git clone "$BASE_URL" "$MYPATH" || {
|
||||
disp E "Git clone failed; previous install kept in $backup_dir."
|
||||
mv "$backup_dir" "$MYPATH" 2>/dev/null || true
|
||||
return 3
|
||||
}
|
||||
if [[ -n "$branch" ]] && ! ( cd "$MYPATH" && git checkout "$branch" ); then
|
||||
disp W "Could not switch to branch '$branch'; staying on the default branch."
|
||||
fi
|
||||
|
||||
# The personal files are not in the repository (profile.conf is
|
||||
# git-ignored): without this the new install would start without
|
||||
# the user's configuration, which would stay in the backup.
|
||||
# Existing files are never overwritten.
|
||||
[[ -f "$backup_dir/profile.conf" ]] && \
|
||||
cp -an "$backup_dir/profile.conf" "$MYPATH/profile.conf"
|
||||
if [[ -d "$backup_dir/profile.d/themes" && -d "$MYPATH/profile.d/themes" ]]; then
|
||||
cp -an "$backup_dir/profile.d/themes/." "$MYPATH/profile.d/themes/"
|
||||
fi
|
||||
disp I "Switched installation to Git source."
|
||||
disp I "Previous install kept in $backup_dir (profile.conf and custom themes were copied)."
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ -d $MYPATH/.git ]] && (( ! use_archive )); then
|
||||
disp I "Git installation detected, applying git pull."
|
||||
command -v git >/dev/null 2>&1 || {
|
||||
disp E "Git is required for this upgrade but is not available."
|
||||
return 3
|
||||
}
|
||||
# git -C instead of cd: no directory change to undo on each exit path
|
||||
local -a G=(git -C "$MYPATH")
|
||||
"${G[@]}" rev-parse --is-inside-work-tree >/dev/null 2>&1 || {
|
||||
disp E "Install directory is not a valid Git working tree."
|
||||
return 3
|
||||
}
|
||||
|
||||
branch="$target_branch"
|
||||
|
||||
# Detect current local branch and warn+switch when it differs
|
||||
local current_branch
|
||||
current_branch=$("${G[@]}" rev-parse --abbrev-ref HEAD 2>/dev/null)
|
||||
if [[ -n "$current_branch" && "$current_branch" != "$branch" ]]; then
|
||||
disp W "Branch mismatch: currently on '$current_branch', configured branch is '$branch'."
|
||||
if (( dry_run )); then
|
||||
disp I "[dry-run] git fetch origin $branch"
|
||||
disp I "[dry-run] git checkout $branch"
|
||||
disp W "[dry-run] Branch would be changed from '$current_branch' to '$branch'."
|
||||
else
|
||||
"${G[@]}" fetch origin "$branch" || {
|
||||
disp E "Git fetch failed for branch $branch."
|
||||
return 2
|
||||
}
|
||||
"${G[@]}" checkout "$branch" || {
|
||||
disp E "Git checkout failed for branch $branch."
|
||||
return 2
|
||||
}
|
||||
disp W "Branch has been changed from '$current_branch' to '$branch'."
|
||||
fi
|
||||
fi
|
||||
|
||||
# status --porcelain: staged, unstaged and untracked (not ignored) files
|
||||
if [[ -n $("${G[@]}" status --porcelain 2>/dev/null) ]]; then
|
||||
if (( force_git )); then
|
||||
disp W "Force mode: local Git changes and untracked files will be lost."
|
||||
if (( dry_run )); then
|
||||
disp I "[dry-run] git fetch --all --prune"
|
||||
disp I "[dry-run] git reset --hard HEAD"
|
||||
disp I "[dry-run] git clean -fd"
|
||||
else
|
||||
"${G[@]}" fetch --all --prune || {
|
||||
disp E "Git fetch failed, upgrade not applied."
|
||||
return 4
|
||||
}
|
||||
"${G[@]}" reset --hard HEAD || {
|
||||
disp E "Git reset failed, upgrade not applied."
|
||||
return 4
|
||||
}
|
||||
"${G[@]}" clean -fd || {
|
||||
disp E "Git clean failed, upgrade not applied."
|
||||
return 4
|
||||
}
|
||||
fi
|
||||
else
|
||||
disp W "The Git working tree contains local changes."
|
||||
disp W "Consider committing or stashing them before upgrading, or use --force."
|
||||
disp W "Upgrade may fail if the changes conflict with the upgrade."
|
||||
fi
|
||||
fi
|
||||
if (( dry_run )); then
|
||||
disp I "[dry-run] git pull --ff-only origin $branch"
|
||||
else
|
||||
# --ff-only: never create a merge commit nor leave conflict markers
|
||||
# in the installed scripts; local commits that diverge are reported
|
||||
"${G[@]}" pull --ff-only origin "$branch" || {
|
||||
disp E "Git pull failed, upgrade not applied (see the git message above: local changes in the way, or local commits that diverge from origin/$branch and must be rebased or dropped by hand)."
|
||||
return 2
|
||||
}
|
||||
disp I "Successfully upgraded using git. Please log out and log in again."
|
||||
fi
|
||||
else
|
||||
if (( use_archive )); then
|
||||
[[ -r "$archive_file" ]] || {
|
||||
disp E "Local archive '$archive_file' is missing or unreadable."
|
||||
return 4
|
||||
}
|
||||
disp I "Using local archive $archive_file."
|
||||
else
|
||||
disp W "No Git repo found. Git is the recommended source."
|
||||
disp I "Applying upgrade from archive..."
|
||||
fi
|
||||
|
||||
if [[ -n "$tmpbase" ]]; then
|
||||
if (( dry_run )); then
|
||||
disp I "[dry-run] mkdir -p \"$tmpbase\""
|
||||
disp I "[dry-run] mktemp -d \"$tmpbase/profile_upg.XXXXXX\""
|
||||
tmpdir="$tmpbase/profile_upg.DRYRUN"
|
||||
else
|
||||
mkdir -p "$tmpbase" || {
|
||||
disp E "Failed to create temporary directory base $tmpbase."
|
||||
return 5
|
||||
}
|
||||
tmpdir=$(mktemp -d "$tmpbase/profile_upg.XXXXXX") || {
|
||||
disp E "Failed to create temp working directory under $tmpbase."
|
||||
return 5
|
||||
}
|
||||
fi
|
||||
else
|
||||
if (( dry_run )); then
|
||||
disp I "[dry-run] mktemp -d /tmp/profile_upg.XXXXXX"
|
||||
tmpdir="/tmp/profile_upg.DRYRUN"
|
||||
else
|
||||
tmpdir=$(mktemp -d /tmp/profile_upg.XXXXXX) || {
|
||||
disp E "Failed to create temporary directory."
|
||||
return 5
|
||||
}
|
||||
fi
|
||||
fi
|
||||
|
||||
if (( use_archive )); then
|
||||
archive="$archive_file"
|
||||
else
|
||||
archive="$tmpdir/profile.tar.gz"
|
||||
if (( dry_run )); then
|
||||
disp I "[dry-run] dwl \"$ARCH_URL\" \"$archive\""
|
||||
else
|
||||
dwl -t 300 "$ARCH_URL" "$archive" || {
|
||||
disp E "Failed to download archive."
|
||||
rm -rf "$tmpdir"
|
||||
return 6
|
||||
}
|
||||
fi
|
||||
fi
|
||||
|
||||
if (( dry_run )); then
|
||||
disp I "[dry-run] tar -xf \"$archive\" -C \"$tmpdir\""
|
||||
disp I "[dry-run] cp -a <extracted_profile>/. \"$MYPATH\"/"
|
||||
else
|
||||
# An archive is refused if one of its members is an absolute path or
|
||||
# climbs out of the extraction directory with "..".
|
||||
if ! tar -tf "$archive" >"$tmpdir/list" 2>/dev/null; then
|
||||
disp E "Archive extraction failed (unreadable archive)."
|
||||
rm -rf "$tmpdir"
|
||||
return 7
|
||||
fi
|
||||
if grep -qE '(^/|(^|/)\.\.(/|$))' "$tmpdir/list"; then
|
||||
disp E "Unsafe archive (absolute or '..' path); upgrade refused."
|
||||
rm -rf "$tmpdir"
|
||||
return 7
|
||||
fi
|
||||
rm -f "$tmpdir/list"
|
||||
|
||||
# --no-same-owner: the files belong to the user running the upgrade
|
||||
tar --no-same-owner -xf "$archive" -C "$tmpdir" || {
|
||||
disp E "Archive extraction failed."
|
||||
rm -rf "$tmpdir"
|
||||
return 7
|
||||
}
|
||||
|
||||
extracted_root=$(find "$tmpdir" -mindepth 1 -maxdepth 1 -type d ! -name '.*' | head -n 1)
|
||||
if [[ -z "$extracted_root" ]]; then
|
||||
disp E "Could not find extracted profile files."
|
||||
rm -rf "$tmpdir"
|
||||
return 8
|
||||
fi
|
||||
|
||||
# cp only adds and replaces: a module removed from the new version
|
||||
# stays in profile.d and keeps being loaded. They are listed after
|
||||
# the copy (never deleted: they may be the user's own modules).
|
||||
local f
|
||||
local -a stale=()
|
||||
for f in "$MYPATH"/profile.d/*.sh "$MYPATH"/profile.d/bash-completion/*.sh; do
|
||||
[[ -e "$f" ]] || continue
|
||||
[[ -e "$extracted_root/${f#"$MYPATH"/}" ]] || stale+=("${f#"$MYPATH"/}")
|
||||
done
|
||||
|
||||
disp I "Installing new version..."
|
||||
cp -a "$extracted_root"/. "$MYPATH"/ || {
|
||||
disp E "Failed to copy new files into $MYPATH."
|
||||
rm -rf "$tmpdir"
|
||||
return 9
|
||||
}
|
||||
|
||||
if (( ${#stale[@]} )); then
|
||||
disp W "These files are not part of the new version and are still loaded; remove them if they are not yours: ${stale[*]}"
|
||||
fi
|
||||
disp I "Upgrade complete. Please log out and log in again."
|
||||
rm -rf "$tmpdir"
|
||||
fi
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
export -f profile_upgrade
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
# EOF
|
||||
+495
-137
@@ -1,43 +1,7 @@
|
||||
#!/bin/bash
|
||||
#!/usr/bin/env bash
|
||||
# Begin profile
|
||||
# ------------------------------------------------------------------------------
|
||||
# Initial version from Beyond Linux From Scratch by
|
||||
# * James Robertson <jameswrobertson@earthlink.net>
|
||||
# * Dagmar d'Surreal <rivyqntzne@pbzpnfg.arg>
|
||||
# ------------------------------------------------------------------------------
|
||||
# Current version from Geoffray Levasseur <fatalerrors@geoffray-levasseur.org>
|
||||
# 16/02/2013 v1.0.0 : Initial version
|
||||
# 24/10/2015 v2.0.0 : Added advanced functionnalities (clean, srr, etc.)
|
||||
# 04/02/2017 v2.0.1 : clean improvements (--shell)
|
||||
# 16/09/2018 v2.1.0 : Added rmhost, setc, setfr, more locales management
|
||||
# 23/09/2019 v2.1.1 : [bugfix] dpkgs
|
||||
# 24/09/2019 v2.1.2 : [bugfix] bug in profile version display
|
||||
# 16/12/2019 v2.2.0 : Added showinfo, primary write of showdiskmap
|
||||
# 08/01/2020 v2.3.0 : Added use of figlet and neofetch as a motd replace
|
||||
# 16/01/2020 v2.3.1 : [bugfix] non-interactive were blocked with some functions
|
||||
# 31/01/2020 v2.3.2 : Figlet: changed default font to ansi_shadow
|
||||
# 02/03/2020 v2.4.0 : Added command auzip
|
||||
# 03/03/2020 v2.5.0 : Added command taz and rmspc, auzip => utaz improved
|
||||
# 05/03/2020 v2.5.1 : Language consistancy fix, added pigz support in taz
|
||||
# 06/03/2020 v2.5.2 : Few aliases sorted out
|
||||
# 11/09/2020 v2.5.3 : Few more aliases, improved code consistancy and typo,
|
||||
# : improved utaz, removed showdiskmap, removed remaining French,
|
||||
# : added license information for future publication
|
||||
# 24/10/2020 v2.6.0 : Added session save and restore for Konsole
|
||||
# 25/12/2020 v2.6.1 : Add check on rmhost, improvements rmspc, created expendlist
|
||||
# 26/02/2021 v2.6.2 : [bugfix] taz: corrected bug with trailing slash on directories
|
||||
# 18/10/2021 v2.6.3 : changed PS1 for status bar style version, few minor improvements
|
||||
# 21/06/2022 v2.7.0 : added isipv4 and isipv6, use it in rmhost as an improvement
|
||||
# 22/06/2022 v2.7.1 : [bugfix] few minor corrections, added help command
|
||||
# 24/06/2022 v2.8.0 : Added backtrace, error and settrace, corrected showinfo
|
||||
# 19/07/2022 v2.8.1 : few cleanup, fixes and optimizations
|
||||
# 29/07/2022 v2.8.2 : added warning for non bash users
|
||||
# 27/08/2022 v3.0.0 : splitted everything, added rain screensaver
|
||||
# 07/11/2022 v3.0.1 : added concatenation to rmspc, added ku, error managed in meteo
|
||||
# 08/11/2022 v3.1.0 : added password generator
|
||||
# 10/11/2022 v3.1.1 : genpwd: test if password is doable
|
||||
# ------------------------------------------------------------------------------
|
||||
# Copyright (c) 2013-2022 Geoffray Levasseur <fatalerrors@geoffray-levasseur.org>
|
||||
# Copyright (c) 2013-2026 Geoffray Levasseur <fatalerrors@geoffray-levasseur.org>
|
||||
# Protected by the BSD3 license. Please read bellow for details.
|
||||
#
|
||||
# * Redistribution and use in source and binary forms,
|
||||
@@ -71,46 +35,370 @@
|
||||
# * OF SUCH DAMAGE.
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
export PROFVERSION="3.1.1"
|
||||
# This first test is POSIX on purpose: it runs before anything bash-specific, so
|
||||
# that sourcing the file from dash, zsh, ... ends with a message instead of
|
||||
# cascading errors. It checks the running shell: $SHELL is the user's login
|
||||
# shell, which says nothing about it (and is unset under cron or systemd).
|
||||
if [ -z "${BASH_VERSION:-}" ]; then
|
||||
echo "That environment script is designed to be used with bash." >&2
|
||||
echo "Please consider using bash to enjoy our features!" >&2
|
||||
# shellcheck disable=SC2317 # exit is the fallback when not sourced
|
||||
return 1 2>/dev/null || exit 1
|
||||
fi
|
||||
|
||||
export DEFAULT_CITY="Toulouse"
|
||||
_profile_is_sourced()
|
||||
{
|
||||
[[ "${BASH_SOURCE[0]}" != "$0" ]]
|
||||
}
|
||||
|
||||
if [[ ! $(echo $SHELL | grep bash) ]]; then
|
||||
echo "That script is designed to be used with bash as being the shell."
|
||||
echo "Please consider using bash instead, or patch me ;) !"
|
||||
# Installation helper: write $2 (the source line) in the file $1, unless that
|
||||
# file already sources $3 (the profile) in some form.
|
||||
_profile_install_in_file()
|
||||
{
|
||||
local rc_file="$1"
|
||||
local source_line="$2"
|
||||
local target="$3"
|
||||
|
||||
[[ -f "$rc_file" ]] || touch "$rc_file" || {
|
||||
printf "[ Error ] Cannot create %s\n" "$rc_file" >&2
|
||||
return 1
|
||||
}
|
||||
|
||||
# The exact line, or any other sourcing statement naming the same file
|
||||
# (". ~/p/profile.sh", with other quotes, ...): adding another one would
|
||||
# load the profile twice.
|
||||
if grep -Fqx "$source_line" "$rc_file" || _profile_file_sources "$rc_file" "$target"; then
|
||||
printf "[ Info ] Already configured in %s\n" "$rc_file"
|
||||
return 0
|
||||
fi
|
||||
|
||||
printf "\n%s\n" "$source_line" >> "$rc_file" || {
|
||||
printf "[ Error ] Cannot write to %s\n" "$rc_file" >&2
|
||||
return 1
|
||||
}
|
||||
|
||||
printf "[ Info ] Added profile source line to %s\n" "$rc_file"
|
||||
return 0
|
||||
}
|
||||
|
||||
# Detect whether $rc_file sources the file whose basename is $target_name.
|
||||
# Returns 0 (true) if a sourcing statement referencing $target_name is found,
|
||||
# ignoring commented-out lines.
|
||||
_profile_file_sources()
|
||||
{
|
||||
local rc_file="$1"
|
||||
local target_name="$2"
|
||||
local line
|
||||
|
||||
[[ -f "$rc_file" ]] || return 1
|
||||
|
||||
while IFS= read -r line || [[ -n "$line" ]]; do
|
||||
# Strip comments so commented-out source lines are not matched.
|
||||
line="${line%%#*}"
|
||||
# Keep only lines whose first word is a source command (`source` or `.`).
|
||||
[[ "$line" =~ ^[[:space:]]*(source|\.)[[:space:]] ]] || continue
|
||||
# Match if the sourcing statement references the target file.
|
||||
[[ "$line" == *"$target_name"* ]] && return 0
|
||||
done < "$rc_file"
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
_profile_install()
|
||||
{
|
||||
local install_bashrc=0
|
||||
local install_profile=0
|
||||
local target_selected=0
|
||||
local script_dir source_line rc=0
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--bashrc)
|
||||
install_bashrc=1
|
||||
target_selected=1
|
||||
;;
|
||||
--profile)
|
||||
install_profile=1
|
||||
target_selected=1
|
||||
;;
|
||||
-h|--help)
|
||||
printf "Usage: %s --install [--bashrc] [--profile]\n" "${BASH_SOURCE[0]}"
|
||||
printf "If no target is specified, both ~/.bashrc and ~/.profile are configured.\n"
|
||||
return 0
|
||||
;;
|
||||
*)
|
||||
printf "[ Error ] Unknown install option: %s\n" "$1" >&2
|
||||
return 2
|
||||
;;
|
||||
esac
|
||||
shift
|
||||
done
|
||||
|
||||
if (( target_selected == 0 )); then
|
||||
install_bashrc=1
|
||||
install_profile=1
|
||||
fi
|
||||
|
||||
# Avoid double loading when both targets are selected and one already
|
||||
# sources the other: only configure the called file so profile.sh is
|
||||
# loaded exactly once.
|
||||
if (( install_bashrc == 1 && install_profile == 1 )); then
|
||||
if _profile_file_sources "$HOME/.bashrc" ".profile"; then
|
||||
printf "[ Info ] ~/.bashrc sources ~/.profile; configuring only ~/.profile to avoid double loading.\n"
|
||||
install_bashrc=0
|
||||
elif _profile_file_sources "$HOME/.profile" ".bashrc"; then
|
||||
printf "[ Info ] ~/.profile sources ~/.bashrc; configuring only ~/.bashrc to avoid double loading.\n"
|
||||
install_profile=0
|
||||
fi
|
||||
fi
|
||||
|
||||
script_dir=$(dirname "$(realpath -s "${BASH_SOURCE[0]}")")
|
||||
# The path goes between double quotes in the rc file: escape what stays
|
||||
# special there (a plain path is left unchanged, so existing lines keep
|
||||
# matching)
|
||||
local escaped_dir="$script_dir"
|
||||
escaped_dir="${escaped_dir//\\/\\\\}"
|
||||
escaped_dir="${escaped_dir//\"/\\\"}"
|
||||
escaped_dir="${escaped_dir//\$/\\\$}"
|
||||
escaped_dir="${escaped_dir//\`/\\\`}"
|
||||
source_line="source \"$escaped_dir/profile.sh\""
|
||||
|
||||
if (( install_bashrc == 1 )); then
|
||||
_profile_install_in_file "$HOME/.bashrc" "$source_line" "$script_dir/profile.sh" || rc=$?
|
||||
fi
|
||||
|
||||
if (( install_profile == 1 )); then
|
||||
_profile_install_in_file "$HOME/.profile" "$source_line" "$script_dir/profile.sh" || rc=$?
|
||||
fi
|
||||
|
||||
return "$rc"
|
||||
}
|
||||
|
||||
# Options are only handled when the file is executed: a sourced file receives
|
||||
# the positional parameters of whoever sources it ("./script --install" sourcing
|
||||
# the profile must not install anything, nor fail on an option that is not for
|
||||
# us).
|
||||
if ! _profile_is_sourced; then
|
||||
case "${1:-}" in
|
||||
--install)
|
||||
shift
|
||||
_profile_install "$@"
|
||||
exit $?
|
||||
;;
|
||||
-h|--help)
|
||||
printf "Usage: source %s\n" "${BASH_SOURCE[0]}"
|
||||
printf " %s --install [--bashrc] [--profile]\n" "${BASH_SOURCE[0]}"
|
||||
exit 0
|
||||
;;
|
||||
"")
|
||||
;;
|
||||
*)
|
||||
printf "[ Error ] Unknown option: %s\n" "$1" >&2
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
|
||||
printf "[ Warning ] profile.sh is designed to be sourced, not executed directly.\n" >&2
|
||||
printf "Use: source \"%s\"\n" "$(realpath -s "${BASH_SOURCE[0]}")" >&2
|
||||
printf "Or run: %s --install [--bashrc] [--profile]\n" "${BASH_SOURCE[0]}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Required for associative arrays (4.0+) and namerefs (4.3+)
|
||||
if ((BASH_VERSINFO[0] < 4)) || [[ ${BASH_VERSINFO[0]} -eq 4 && ${BASH_VERSINFO[1]} -lt 3 ]]; then
|
||||
echo "[ Error ] This profile requires Bash 4.3 or higher." >&2
|
||||
echo "Current version: $BASH_VERSION" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# path* : private functions for PATH variable management
|
||||
# ------------------------------------------------------------------------------
|
||||
pathremove ()
|
||||
pathremove()
|
||||
{
|
||||
local ifs=':'
|
||||
local newpath
|
||||
local dir
|
||||
local pathvar=${2:-PATH}
|
||||
for dir in ${!pathvar} ; do
|
||||
if [ "$dir" != "$1" ] ; then
|
||||
newpath=${newpath:+$newpath:}$dir
|
||||
[[ -z "$1" ]] && return 0
|
||||
local IFS=':'
|
||||
local newpath dir
|
||||
local pathvar="${2:-PATH}"
|
||||
[[ "$pathvar" =~ ^[a-zA-Z_][a-zA-Z0-9_]*$ ]] || {
|
||||
printf "pathremove: unsafe variable name '%s'\n" "$pathvar" >&2
|
||||
return 1
|
||||
}
|
||||
for dir in ${!pathvar}; do
|
||||
[[ "$dir" != "$1" ]] && newpath="${newpath:+$newpath:}$dir"
|
||||
done
|
||||
export "$pathvar=$newpath"
|
||||
}
|
||||
|
||||
#pathprepend() # Unused for now, but might be useful in the future
|
||||
#{
|
||||
# [[ -z "$1" ]] && return 0
|
||||
# local pathvar="${2:-PATH}"
|
||||
# [[ "$pathvar" =~ ^[a-zA-Z_][a-zA-Z0-9_]*$ ]] || {
|
||||
# printf "pathprepend: unsafe variable name '%s'\n" "$pathvar" >&2
|
||||
# return 1
|
||||
# }
|
||||
# pathremove "$1" "$pathvar"
|
||||
# export "$pathvar=$1${!pathvar:+:${!pathvar}}"
|
||||
#}
|
||||
|
||||
# Append one directory to a path variable (PATH by default), unless it is
|
||||
# already in it: an entry that exists keeps its place. Moving it to the end
|
||||
# would, for instance, demote ~/.local/bin that Debian's ~/.profile puts first,
|
||||
# so that system programs shadow the user's own ones.
|
||||
pathappend()
|
||||
{
|
||||
[[ -z "$1" ]] && return 0
|
||||
local pathvar="${2:-PATH}"
|
||||
[[ "$pathvar" =~ ^[a-zA-Z_][a-zA-Z0-9_]*$ ]] || {
|
||||
printf "pathappend: unsafe variable name '%s'\n" "$pathvar" >&2
|
||||
return 1
|
||||
}
|
||||
case ":${!pathvar}:" in
|
||||
*":$1:"*) return 0 ;;
|
||||
esac
|
||||
export "$pathvar=${!pathvar:+${!pathvar}:}$1"
|
||||
}
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# Configuration file parser
|
||||
parse_conf()
|
||||
{
|
||||
local config_file="$1"
|
||||
local current_section=""
|
||||
local key value
|
||||
|
||||
[[ ! -f "$config_file" ]] && return 1
|
||||
|
||||
while IFS='=' read -r key value || [[ -n "$key" ]]; do
|
||||
# Internal trimming (removes leading/trailing whitespace & CR)
|
||||
key="${key%"${key##*[![:space:]]}"}"
|
||||
key="${key#"${key%%[![:space:]]*}"}"
|
||||
key="${key%$'\r'}" # Strip potential Windows line endings
|
||||
|
||||
# Skip comments and empty lines
|
||||
[[ -z "$key" || "$key" =~ ^[#\;] ]] && continue
|
||||
|
||||
# Section Detection: [section_name], optionally followed by a comment
|
||||
if [[ "$key" =~ ^\[([a-zA-Z0-9_]+)\][[:space:]]*([#\;].*)?$ ]]; then
|
||||
current_section="${BASH_REMATCH[1]}"
|
||||
declare -g -A "CONF_$current_section"
|
||||
continue
|
||||
fi
|
||||
|
||||
# A key is a variable name: it ends up in "export" (load_conf) and as an
|
||||
# array subscript. Anything else (a stray line, a typo) is reported.
|
||||
if [[ ! "$key" =~ ^[A-Za-z_][A-Za-z0-9_]*$ ]]; then
|
||||
printf "[ Warning ] %s: invalid configuration line ignored: '%s'\n" \
|
||||
"$config_file" "$key" >&2
|
||||
continue
|
||||
fi
|
||||
|
||||
# Secure Assignment (if inside a section)
|
||||
if [[ -n "$current_section" ]]; then
|
||||
# Clean the value
|
||||
value="${value%"${value##*[![:space:]]}"}"
|
||||
value="${value#"${value%%[![:space:]]*}"}"
|
||||
value="${value%$'\r'}"
|
||||
|
||||
# A quoted value is what is between the quotes (a trailing comment
|
||||
# is dropped); an unquoted one ends at an inline comment, which is
|
||||
# a "#" preceded by a blank ("dark # my theme").
|
||||
if [[ "$value" == \"* ]]; then
|
||||
value="${value#\"}"
|
||||
value="${value%%\"*}"
|
||||
elif [[ "$value" == \'* ]]; then
|
||||
value="${value#\'}"
|
||||
value="${value%%\'*}"
|
||||
else
|
||||
value="${value%%[[:space:]]#*}"
|
||||
value="${value%"${value##*[![:space:]]}"}"
|
||||
fi
|
||||
|
||||
# Protect against command injection by disallowing certain characters in keys
|
||||
value="${value//\`/}"
|
||||
value="${value//\$\(/}"
|
||||
|
||||
# Correctly interpretet internal variables (e.g. $HOME). In the
|
||||
# [prompt] section only the variables that exist are substituted:
|
||||
# envsubst would otherwise replace an unknown one by nothing,
|
||||
# wiping out the $Blue-style colour references that are resolved
|
||||
# later, when the colour variables exist (see prompt.sh).
|
||||
if [[ "$value" == *\$* ]]; then
|
||||
if command -v envsubst >/dev/null 2>&1; then
|
||||
if [[ "$current_section" == "prompt" ]]; then
|
||||
local _envnames="" _envname
|
||||
for _envname in $(compgen -e); do
|
||||
[[ "$_envname" =~ ^[A-Za-z_][A-Za-z0-9_]*$ ]] && _envnames+="\${${_envname}}"
|
||||
done
|
||||
value=$(envsubst "$_envnames" <<< "$value")
|
||||
else
|
||||
value=$(envsubst <<< "$value")
|
||||
fi
|
||||
fi
|
||||
# If envsubst is unavailable, $VAR references are left as-is.
|
||||
fi
|
||||
|
||||
# Use a nameref for safe, eval-free assignment
|
||||
local -n current_array="CONF_$current_section"
|
||||
# shellcheck disable=SC2034 # Dynamic var creation
|
||||
current_array["$key"]="$value"
|
||||
fi
|
||||
done < "$config_file"
|
||||
}
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# Load command aliases from configuration
|
||||
load_alias()
|
||||
{
|
||||
local section_name="CONF_$1"
|
||||
|
||||
# Check if the associative array exists using declare -p
|
||||
[[ "$(declare -p "$section_name" 2>/dev/null)" != "declare -A"* ]] && return 1
|
||||
|
||||
# Create a nameref to the section array
|
||||
local -n current_aliases="$section_name"
|
||||
local key
|
||||
|
||||
# Iterate safely over the keys of the associative array
|
||||
for key in "${!current_aliases[@]}"; do
|
||||
local cmd="${current_aliases[$key]}"
|
||||
|
||||
# Extract the base command (first word) safely without awk
|
||||
local base_cmd="${cmd%% *}"
|
||||
|
||||
# Only alias if the base command is executable
|
||||
if command -v "$base_cmd" >/dev/null 2>&1; then
|
||||
# shellcheck disable=SC2139 # Dynamic alias creation
|
||||
alias "$key"="$cmd"
|
||||
fi
|
||||
done
|
||||
export $pathvar="$newpath"
|
||||
}
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
pathprepend ()
|
||||
{
|
||||
pathremove $1 $2
|
||||
local pathvar=${2:-PATH}
|
||||
export $pathvar="$1${!pathvar:+:${!pathvar}}"
|
||||
}
|
||||
|
||||
pathappend ()
|
||||
# ------------------------------------------------------------------------------
|
||||
# Load configuration values as environment variables
|
||||
load_conf()
|
||||
{
|
||||
pathremove $1 $2
|
||||
local pathvar=${2:-PATH}
|
||||
export $pathvar="${!pathvar:+${!pathvar}:}$1"
|
||||
local section_name="CONF_$1"
|
||||
|
||||
# Missing section is not an error: modules can rely on built-in defaults.
|
||||
[[ "$(declare -p "$section_name" 2>/dev/null)" != "declare -A"* ]] && return 0
|
||||
|
||||
local -n current_vars="$section_name"
|
||||
local key
|
||||
|
||||
for key in "${!current_vars[@]}"; do
|
||||
# Export the key/value pair as a standard shell variable
|
||||
# We use 'export' directly; Bash handles the assignment safely here
|
||||
export "$key"="${current_vars[$key]}"
|
||||
done
|
||||
}
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
@@ -119,88 +407,158 @@ pathappend ()
|
||||
# ------------------------------------------------------------------------------
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
# Build PATH environment variable
|
||||
if [[ $EUID -eq 0 ]] ; then
|
||||
pathappend /sbin:/usr/sbin
|
||||
# Store the script's path. Because we're more likely to be sourced, we use
|
||||
# BASH_SOURCE to get the path of the sourced file instead of $0. If profile.sh
|
||||
# itself is a symbolic link, it is followed to find the real installation
|
||||
# (profile.d, version, ...); a symlinked directory is kept as the user sees it
|
||||
# (realpath -s).
|
||||
if [[ -z "${PROFILE_PATH:-}" ]]; then
|
||||
_profile_src="${BASH_SOURCE[0]}"
|
||||
[[ -L "$_profile_src" ]] && _profile_src=$(readlink -f -- "$_profile_src")
|
||||
MYPATH=$(dirname "$(realpath -s -- "$_profile_src")")
|
||||
unset _profile_src
|
||||
else
|
||||
MYPATH="$PROFILE_PATH"
|
||||
fi
|
||||
export MYPATH
|
||||
if [[ ! -e "$MYPATH/profile.sh" ]]; then
|
||||
echo "[ Warning ] Path detection failed, trying to use pwd..." >&2
|
||||
MYPATH=$(pwd)
|
||||
if [[ ! -e "$MYPATH/profile.sh" ]]; then
|
||||
echo "[ Error ] Unable to determine installation path, pretty much nothing will work." >&2
|
||||
fi
|
||||
fi
|
||||
|
||||
PROFVERSION=""
|
||||
if [[ -s "$MYPATH/version" ]]; then
|
||||
IFS= read -r PROFVERSION < "$MYPATH/version"
|
||||
PROFVERSION="${PROFVERSION%$'\r'}"
|
||||
else
|
||||
echo "[ Warning ] Unable to determine running profile version; your installation might be broken." >&2
|
||||
fi
|
||||
export PROFVERSION
|
||||
|
||||
# Build PATH environment variable
|
||||
if [[ $EUID -eq 0 ]]; then
|
||||
pathappend /sbin
|
||||
pathappend /usr/sbin
|
||||
fi
|
||||
[[ -d /share/services/gestparc ]] && pathappend /share/services/gestparc
|
||||
[[ -d ~/bin ]] && pathappend ~/bin
|
||||
[[ -d ~/.local/bin ]] && pathappend ~/.local/bin
|
||||
|
||||
# Set bash history
|
||||
export HISTSIZE=50000
|
||||
export HISTIGNORE="&:[bf]g:exit"
|
||||
# Parse and load general configuration
|
||||
export PROFILE_CONF="$MYPATH/profile.conf"
|
||||
parse_conf "$PROFILE_CONF"
|
||||
# Overload with user configuration if it exists
|
||||
if [[ -f "$HOME/.profile.conf" ]]; then
|
||||
parse_conf "$HOME/.profile.conf"
|
||||
fi
|
||||
load_conf system # Load Bash system behavior configuration (history, pager, etc.)
|
||||
load_conf general # General purpose configuration (compilation flags, etc.)
|
||||
|
||||
# Set default pager
|
||||
export PAGER=less
|
||||
|
||||
# More colors
|
||||
export TERM=xterm-256color
|
||||
|
||||
# Set some compiling values
|
||||
export CFLAGS="-O2 -pipe -march=native"
|
||||
export MAKEFLAGS='-j12'
|
||||
export PKGSOURCES='/share/src/archives'
|
||||
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# Default values could be altered after this line
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
# Load personal configuration
|
||||
[[ -f ~/.profile.conf ]] && . ~/.profile.conf
|
||||
|
||||
# Execute optionnal config script if any
|
||||
for script in ~/profile.d/*.sh ; do
|
||||
if [ -r $script ] ; then
|
||||
. $script
|
||||
fi
|
||||
done
|
||||
|
||||
# Interactive shell detection, two methods available each one of those might have different result
|
||||
# depending on distribution
|
||||
#shopt -q login_shell && INTERACTIVE=1
|
||||
[[ $- == *i* ]] && INTERACTIVE=1
|
||||
|
||||
if [[ $INTERACTIVE ]]; then
|
||||
# For compiling (as we often compile with LFS/0linux...)
|
||||
#Aliases
|
||||
alias ll='ls -laFh --color=auto'
|
||||
alias la='ls -Ah --color=auto'
|
||||
alias l='ls -CF --color=auto'
|
||||
alias ls='ls --color=auto'
|
||||
|
||||
alias grep='grep --color=auto'
|
||||
alias egrep='egrep --color=auto'
|
||||
alias fgrep='fgrep --color=auto'
|
||||
alias qfind="find . -name "
|
||||
|
||||
alias mkck='make check'
|
||||
alias mkin='make install'
|
||||
alias mkdin='make DESTDIR=$PWD/dest-install install'
|
||||
alias ssh='ssh -Y'
|
||||
|
||||
alias wget='wget -c' # resume mode by default
|
||||
alias myip='curl ip.appspot.com'
|
||||
|
||||
# Human readable by default
|
||||
alias df='df -H'
|
||||
alias du='du -ch'
|
||||
|
||||
alias sdu='du -sk ./* | sort -n'
|
||||
|
||||
# Define PS1
|
||||
trap 'timer_start' DEBUG
|
||||
PROMPT_COMMAND='set_prompt'
|
||||
|
||||
# Set default language
|
||||
setfr
|
||||
showinfo
|
||||
echo "Profile version $PROFVERSION chargé..."
|
||||
# Helpers shared by several modules (each module also loads the common helpers
|
||||
# itself when it is sourced alone, see _profile_load_helpers).
|
||||
if [[ -r "$MYPATH/helpers/common.sh" ]]; then
|
||||
# shellcheck source=helpers/common.sh
|
||||
. "$MYPATH/helpers/common.sh"
|
||||
else
|
||||
printf "[ Error ] Common helpers not found: %s\n" "$MYPATH/helpers/common.sh" >&2
|
||||
# shellcheck disable=SC2317 # exit is the fallback when not sourced
|
||||
return 1 2>/dev/null || exit 1
|
||||
fi
|
||||
|
||||
# Load module scripts. nullglob is only needed for the expansion: the caller's
|
||||
# setting is put back afterwards instead of being forced off.
|
||||
_profile_ng=0
|
||||
shopt -q nullglob && _profile_ng=1
|
||||
shopt -s nullglob
|
||||
for _profile_script in "$MYPATH/profile.d/"*.sh; do
|
||||
if [[ -f "$_profile_script" && -r "$_profile_script" ]]; then
|
||||
# shellcheck source=/dev/null
|
||||
. "$_profile_script" || printf "[ Warning ] Failed to source module: %s\n" "$_profile_script" >&2
|
||||
fi
|
||||
done
|
||||
unset _profile_script
|
||||
(( _profile_ng )) || shopt -u nullglob
|
||||
|
||||
# Interactive shell detection. INTERACTIVE is exported for the modules, so it
|
||||
# is reset when the shell is not interactive: a non-interactive child of an
|
||||
# interactive shell would otherwise inherit INTERACTIVE=1 and run the whole
|
||||
# interactive part (banner, prompt, update check) inside scripts.
|
||||
if [[ $- == *i* ]]; then
|
||||
export INTERACTIVE=1
|
||||
else
|
||||
unset INTERACTIVE
|
||||
fi
|
||||
|
||||
if [[ -n "${INTERACTIVE:-}" ]]; then
|
||||
# Load custom bash completions
|
||||
shopt -s nullglob
|
||||
for _compl in "$MYPATH/profile.d/bash-completion/"*.sh; do
|
||||
# shellcheck disable=SC1090 # Dynamic sourcing of completion scripts
|
||||
[[ -f "$_compl" && -r "$_compl" ]] && . "$_compl"
|
||||
done
|
||||
unset _compl
|
||||
(( _profile_ng )) || shopt -u nullglob
|
||||
|
||||
# Aliases
|
||||
load_alias aliases
|
||||
|
||||
# Define PS1. The timer hook is a DEBUG trap: it replaces any DEBUG trap set
|
||||
# before (it cannot even be read from a sourced file, bash hides it there),
|
||||
# so load this profile before tools such as bash-preexec.
|
||||
trap 'timer_start' DEBUG
|
||||
|
||||
# set_prompt is added to PROMPT_COMMAND instead of replacing it, so that
|
||||
# what exists (VTE's directory reporting, direnv, "history -a", ...) keeps
|
||||
# working. It has to read the exit status of the user's last command, which
|
||||
# any command running before it resets: _profile_save_status stores it
|
||||
# first, set_prompt (which uses it) runs last. It must also run last for the
|
||||
# timer: the DEBUG trap fires for every command of PROMPT_COMMAND, and the
|
||||
# elapsed time is measured from the first one of the user's command.
|
||||
# Nothing changes when PROMPT_COMMAND is empty or already has set_prompt.
|
||||
case "$(declare -p PROMPT_COMMAND 2>/dev/null)" in
|
||||
"declare -a"*)
|
||||
# Array form (bash 5.1+)
|
||||
if [[ " ${PROMPT_COMMAND[*]} " != *" set_prompt "* ]]; then
|
||||
if (( ${#PROMPT_COMMAND[@]} )); then
|
||||
PROMPT_COMMAND=(_profile_save_status "${PROMPT_COMMAND[@]}" set_prompt)
|
||||
else
|
||||
PROMPT_COMMAND=(set_prompt)
|
||||
fi
|
||||
fi
|
||||
;;
|
||||
*)
|
||||
if [[ -z "${PROMPT_COMMAND:-}" ]]; then
|
||||
PROMPT_COMMAND='set_prompt'
|
||||
elif [[ "$PROMPT_COMMAND" != *set_prompt* ]]; then
|
||||
PROMPT_COMMAND="_profile_save_status;${PROMPT_COMMAND};set_prompt"
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
|
||||
# Set default language from DEFAULT_LANG config key (set in [general]).
|
||||
# The value must match one of the alias names defined in SET_LOCALE so that
|
||||
# the corresponding set<alias> function exists after build_locale_shortcuts.
|
||||
if [[ -n "${DEFAULT_LANG:-}" ]]; then
|
||||
_lang_fn="set${DEFAULT_LANG}"
|
||||
if declare -F "$_lang_fn" >/dev/null 2>&1; then
|
||||
"$_lang_fn"
|
||||
else
|
||||
disp W "DEFAULT_LANG '$DEFAULT_LANG' has no matching locale shortcut (check SET_LOCALE in profile.conf)."
|
||||
fi
|
||||
unset _lang_fn
|
||||
fi
|
||||
showinfo && printf "\n"
|
||||
check_updates -q
|
||||
disp I "Profile version $PROFVERSION loaded..."
|
||||
fi
|
||||
unset _profile_ng
|
||||
|
||||
# Cleanup
|
||||
unset pathremove pathprepend pathappend
|
||||
unset -f _profile_is_sourced _profile_install_in_file _profile_file_sources _profile_install
|
||||
unset -f parse_conf load_alias load_conf
|
||||
unset -f pathremove pathprepend pathappend
|
||||
|
||||
#return 0
|
||||
|
||||
|
||||
Reference in New Issue
Block a user