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 |
@@ -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,23 +1,45 @@
|
|||||||
# profile
|
# profile
|
||||||
|
|
||||||
This project aims to create an advanced bash profile. It includes aliases,
|
This project aims to create an advanced bash profile. It includes aliases,
|
||||||
a customized prompt and several functions for different purposes. It's mostly
|
a customized prompt and several functions for different purposes. It's mostly
|
||||||
targeted to system administrators but might satisfy some regular users.
|
targeted to system administrators but might satisfy some regular users.
|
||||||
|
|
||||||
## 1. Requirements
|
## 1. Requirements
|
||||||
|
|
||||||
profile requires **Bash 4.3 or higher** (for associative arrays and namerefs).
|
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
|
It will refuse to load on older versions and will also refuse to load if the
|
||||||
current shell is not bash.
|
current shell is not bash.
|
||||||
|
|
||||||
## 2. Getting started
|
## 2. Getting started
|
||||||
|
|
||||||
Download and extract (or use git clone) the profile archive into your home
|
Download and extract (or use git clone) the profile archive into your home
|
||||||
directory. You will have to modify your `~/.bashrc` and/or `~/.profile` file to
|
directory.
|
||||||
add at the end (preferably):
|
|
||||||
|
The profile is designed to be **sourced**, not executed directly.
|
||||||
|
|
||||||
|
Manual setup:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
source <installpath>/profile/profile.sh
|
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
|
You may also set the `PROFILE_PATH` environment variable before sourcing if you
|
||||||
want to override the automatic path detection:
|
want to override the automatic path detection:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
export PROFILE_PATH=/opt/profile
|
export PROFILE_PATH=/opt/profile
|
||||||
source /opt/profile/profile.sh
|
source /opt/profile/profile.sh
|
||||||
@@ -26,64 +48,118 @@ source /opt/profile/profile.sh
|
|||||||
It's not recommended to load that profile in `/etc/profile` as users' `.bashrc`
|
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.
|
files might interfere with some aliases and functions defined in profile.
|
||||||
|
|
||||||
### 2.1. Initial configuration
|
### 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:
|
Copy the example configuration file and customise it to your needs:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cp <installpath>/profile/doc/profile.conf.example <installpath>/profile/profile.conf
|
cp <installpath>/profile/doc/profile.conf.example <installpath>/profile/profile.conf
|
||||||
```
|
```
|
||||||
|
|
||||||
`profile.conf` is git-ignored so your personal settings will never be
|
`profile.conf` is git-ignored so your personal settings will never be
|
||||||
accidentally committed. All keys are optional — sensible defaults apply when
|
accidentally committed. All keys are optional — sensible defaults apply when
|
||||||
unset. See [section 4](#4-configuration) for the full reference.
|
unset. See [section 4](#4-configuration) for the full reference.
|
||||||
|
|
||||||
## 3. What's the purpose?
|
## 3. What's the purpose?
|
||||||
|
|
||||||
profile gives access to numerous functions, aliases and to an advanced prompt.
|
profile gives access to numerous functions, aliases and to an advanced prompt.
|
||||||
All functions are organized into modules under the `profile.d/` directory and
|
All functions are organized into modules under the `profile.d/` directory and
|
||||||
are loaded automatically at startup.
|
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
|
### 3.1. Prompt
|
||||||
|
|
||||||
A bar-style prompt showing current time, execution time of the last command
|
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.
|
(with sub-millisecond precision), and the exit code of the last command.
|
||||||
|
|
||||||
### 3.2. Functions reference
|
### 3.2. Functions reference
|
||||||
|
|
||||||
| Function | Module | Description |
|
| Function | Module | Description |
|
||||||
|---|---|---|
|
| --- | --- | --- |
|
||||||
| `busy` | fun | Monitor /dev/urandom for a hex pattern — look busy |
|
| `backtrace` | debug | Print the current call stack (function, file, line of each call), innermost frame first; used by the `settrace` error trace |
|
||||||
| `check_updates` | updates | Check whether a newer profile version is available online |
|
| `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 |
|
| `clean` | filefct | Erase backup files in given directories, optionally recursive |
|
||||||
| `disp` | disp | Display formatted info / warning / error / debug messages |
|
| `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` |
|
||||||
| `dwl` | net | Download a URL using curl, wget, or fetch transparently |
|
| `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) |
|
||||||
| `expandlist` | filefct | Expand glob expressions into a quoted, separated list |
|
| `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 |
|
||||||
| `file_stats` | filefct | Display file size statistics for a path |
|
| `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 |
|
| `findbig` | filefct | Find the biggest files in the given or current directory |
|
||||||
| `finddead` | filefct | Find dead symbolic links 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 |
|
| `findzero` | filefct | Find empty files in the given or current directory; `--delete` does not follow symlinks |
|
||||||
| `genpwd` | pwd | Generate one or more random secure passwords with configurable constraints |
|
| `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) |
|
| `gpid` | processes | Give the list of PIDs matching the given process name(s) |
|
||||||
| `help` | help | Display the list of available functions and basic usage |
|
| `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 |
|
| `isipv4` | net | Tell if the given parameter is a valid IPv4 address |
|
||||||
| `isipv6` | net | Tell if the given parameter is a valid IPv6 address |
|
| `isipv6` | net | Tell if the given parameter is a valid IPv6 address |
|
||||||
| `ku` | processes | Kill all processes owned by the given user name or ID |
|
| `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) |
|
| `matrix` | rain | Console screensaver with Matrix-style digital rain (binary, kana, ascii charset) |
|
||||||
| `mcd` | filefct | Create a directory and immediately move into it |
|
| `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 |
|
| `meteo` | info | Display weather forecast for the configured or given city |
|
||||||
| `myextip` | net | Get information about your public IP address |
|
| `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`) |
|
| `get_pkgmgr` | packages | Detect the active package manager of the running distribution (`apt`, `dnf`, `yum`, `zypper`, `pacman`, `apk`, `portage`, `xbps`, `nix`) |
|
||||||
| `pkgs` | packages | Search for a pattern in installed package names (distro-aware via `get_pkgmgr`, supports `-i`) |
|
| `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 |
|
| `ppg` | processes | Look for the given pattern in running processes |
|
||||||
| `ppn` | processes | List processes matching an exact command name |
|
| `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 |
|
| `ppu` | processes | List processes owned by a specific user |
|
||||||
| `profile_upgrade` | updates | Upgrade profile to the latest version (git pull or archive) |
|
| `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 |
|
| `pwdscore` | pwd | Calculate the strength score of a given password |
|
||||||
| `rain` | rain | Console screensaver with falling-rain effect (multiple color themes) |
|
| `rain` | rain | Console screensaver with falling-rain effect (multiple color themes) |
|
||||||
| `rmhost` | ssh | Remove host (name and IP) from SSH known_hosts; supports `--all-users` as root |
|
| `rainbow` | rain | Full-screen rainbow screensaver using only background colors, with horizontal color shifting |
|
||||||
| `rmspc` | filefct | Replace spaces in filenames with underscores (or a custom character) |
|
| `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) |
|
| `setc` | lang | Set locale to standard C (POSIX) |
|
||||||
| `setlocale` | lang | Set console locale to any installed locale |
|
| `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 |
|
||||||
| `settrace` | debug | Activate or deactivate ERR trap to display backtrace on script errors |
|
| `set_colors` | disp | (Re)define the colour variables (`Red`, `BIWhite`, `On_Blue`, `RESETCOL`, ...) used by `disp`, the prompt and themes; run at load time |
|
||||||
| `showinfo` | info | Display welcome banner and system information (figlet + neofetch/fastfetch) |
|
| `set_theme` | prompt | Switch the prompt colour theme for the session (`--list`, `--preview`, `--save`) |
|
||||||
| `ssr` | ssh | SSH into a server as root, forwarding extra ssh options |
|
| `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 |
|
| `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 |
|
| `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) |
|
| `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 |
|
| `ver` | info | Display the installed profile version |
|
||||||
@@ -91,7 +167,15 @@ A bar-style prompt showing current time, execution time of the last command
|
|||||||
Locale shortcut functions (`setfr`, `setus`, etc.) are dynamically generated at
|
Locale shortcut functions (`setfr`, `setus`, etc.) are dynamically generated at
|
||||||
startup from the `SET_LOCALE` configuration key (see section 4).
|
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
|
## 4. Configuration
|
||||||
|
|
||||||
profile uses an INI-style configuration file (`profile.conf`) located in the
|
profile uses an INI-style configuration file (`profile.conf`) located in the
|
||||||
same directory as `profile.sh`. Sections are declared with `[section_name]` and
|
same directory as `profile.sh`. Sections are declared with `[section_name]` and
|
||||||
keys follow `key = value` syntax. Each module calls `load_conf "<section>"` at
|
keys follow `key = value` syntax. Each module calls `load_conf "<section>"` at
|
||||||
@@ -99,18 +183,55 @@ load time, which exports every key in that section as an environment variable.
|
|||||||
Unknown keys are silently ignored; all keys are optional — sensible defaults
|
Unknown keys are silently ignored; all keys are optional — sensible defaults
|
||||||
apply when unset.
|
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,
|
`profile.conf` is listed in `.gitignore` so personal values (API keys, cities,
|
||||||
compiler flags, …) are never accidentally staged. Start from the annotated
|
compiler flags, …) are never accidentally staged. Start from the annotated
|
||||||
template at `doc/profile.conf.example` (see [section 2.1](#21-initial-configuration)).
|
template at `doc/profile.conf.example` (see [section 2.2](#22-initial-configuration)).
|
||||||
|
|
||||||
### 4.1. Core sections
|
### 4.1. Core sections
|
||||||
|
|
||||||
| Section | Purpose |
|
| Section | Purpose |
|
||||||
|---|---|
|
| --- | --- |
|
||||||
| `[system]` | Bash history size, pager, and other shell behaviours |
|
| `[system]` | Bash history size, pager, terminal type, and other shell behaviours |
|
||||||
| `[general]` | General-purpose variables (e.g. compilation flags, `MAKEFLAGS`) |
|
| `[general]` | General-purpose variables (e.g. compilation flags, `MAKEFLAGS`) |
|
||||||
| `[aliases]` | User command aliases, loaded for interactive shells only |
|
| `[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
|
### 4.2. Module defaults
|
||||||
|
|
||||||
Each module exposes its hardcoded defaults as configuration keys. Set a key to
|
Each module exposes its hardcoded defaults as configuration keys. Set a key to
|
||||||
@@ -119,17 +240,24 @@ change the default without having to pass flags every time.
|
|||||||
**`[compress]`** — `taz` / `utaz`
|
**`[compress]`** — `taz` / `utaz`
|
||||||
|
|
||||||
| Key | Default | Description |
|
| Key | Default | Description |
|
||||||
|---|---|---|
|
| --- | --- | --- |
|
||||||
| `TAZ_DEFAULT_FORMAT` | `tar.gz` | Archive format for `taz` (`tar.gz`, `tar.bz2`, `tar.xz`, `zip`, …) |
|
| `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` | `0` | Compression threads (0 = auto-detect) |
|
| `TAZ_DEFAULT_THREADS` | `auto` | Compression threads (`auto` = runtime CPU count, or explicit positive integer) |
|
||||||
| `TAZ_DEFAULT_LEVEL` | `6` | Compression level (1–9) |
|
| `TAZ_DEFAULT_LEVEL` | `6` | Compression level (1–9) |
|
||||||
| `UTAZ_DEFAULT_DELETE` | `0` | Set to `1` to delete the source archive after extraction |
|
| `UTAZ_DEFAULT_DELETE` | `0` | Set to `1` to delete the source archive after extraction |
|
||||||
| `UTAZ_DEFAULT_DIR_MODE` | `0` | Set to `1` to always extract into a subdirectory |
|
| `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
|
**`[filefct]`** — file utilities
|
||||||
|
|
||||||
| Key | Default | Description |
|
| Key | Default | Description |
|
||||||
|---|---|---|
|
| --- | --- | --- |
|
||||||
| `EXPANDLIST_DEFAULT_SEPARATOR` | ` ` (space) | Separator used by `expandlist` |
|
| `EXPANDLIST_DEFAULT_SEPARATOR` | ` ` (space) | Separator used by `expandlist` |
|
||||||
| `CLEAN_DEFAULT_RECURSIVE` | `0` | Set to `1` to make `clean` recurse into subdirectories |
|
| `CLEAN_DEFAULT_RECURSIVE` | `0` | Set to `1` to make `clean` recurse into subdirectories |
|
||||||
| `RMSPC_DEFAULT_CHAR` | `_` | Replacement character used by `rmspc` |
|
| `RMSPC_DEFAULT_CHAR` | `_` | Replacement character used by `rmspc` |
|
||||||
@@ -138,25 +266,28 @@ change the default without having to pass flags every time.
|
|||||||
**`[rain]`** — screensavers
|
**`[rain]`** — screensavers
|
||||||
|
|
||||||
| Key | Default | Description |
|
| Key | Default | Description |
|
||||||
|---|---|---|
|
| --- | --- | --- |
|
||||||
| `RAIN_DEFAULT_SPEED` | `0.1` | Falling speed for `rain` |
|
| `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` | `Green` | Colour for `rain` |
|
| `RAIN_DEFAULT_COLOR` | `white` | Colour for `rain` (`green`, `blue`, `red`, `yellow`, `cyan`, `white`; case-insensitive) |
|
||||||
| `MATRIX_DEFAULT_SPEED` | `0.05` | Falling speed for `matrix` |
|
| `RAIN_DEFAULT_DENSITY` | dynamic | Maximum number of simultaneous falling elements for `rain` |
|
||||||
| `MATRIX_DEFAULT_COLOR` | `Green` | Colour for `matrix` |
|
| `RAINBOW_DEFAULT_SPEED` | `4` | Frame delay (hundredths of a second) of the colour shift for `rainbow` |
|
||||||
| `MATRIX_DEFAULT_CHARSET` | `binary` | Character set for `matrix` (`binary`, `kana`, `ascii`) |
|
| `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]`**
|
**`[ssh]`**
|
||||||
|
|
||||||
| Key | Default | Description |
|
| Key | Default | Description |
|
||||||
|---|---|---|
|
| --- | --- | --- |
|
||||||
| `SSH_DEFAULT_OPT` | _(empty)_ | Extra options passed to `ssr` (e.g. `-Y` for X forwarding) |
|
| `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
|
**`[pwd]`** — password tools
|
||||||
|
|
||||||
| Key | Default | Description |
|
| Key | Default | Description |
|
||||||
|---|---|---|
|
| --- | --- | --- |
|
||||||
| `GENPWD_DEFAULT_LENGTH` | `16` | Generated password length |
|
| `GENPWD_DEFAULT_LENGTH` | `16` | Generated password length |
|
||||||
| `GENPWD_DEFAULT_OCCURS` | `1` | Number of character class occurrences |
|
| `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_COUNT` | `1` | Number of passwords to generate |
|
||||||
| `GENPWD_DEFAULT_SYMBOLS` | `1` | Include symbols (0/1) |
|
| `GENPWD_DEFAULT_SYMBOLS` | `1` | Include symbols (0/1) |
|
||||||
| `GENPWD_DEFAULT_UPPERCASE` | `1` | Include uppercase letters (0/1) |
|
| `GENPWD_DEFAULT_UPPERCASE` | `1` | Include uppercase letters (0/1) |
|
||||||
@@ -167,41 +298,56 @@ change the default without having to pass flags every time.
|
|||||||
**`[fun]`**
|
**`[fun]`**
|
||||||
|
|
||||||
| Key | Default | Description |
|
| Key | Default | Description |
|
||||||
|---|---|---|
|
| --- | --- | --- |
|
||||||
| `BUSY_DEFAULT_PATTERN` | `[0-9a-f]` | Hex pattern matched by `busy` |
|
| `BUSY_DEFAULT_PATTERN` | `ca fe` | String searched (fixed string, case-insensitive) in the hexdump lines printed by `busy` |
|
||||||
| `BUSY_DEFAULT_DELAY` | `0.1` | Polling delay (seconds) for `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]`**
|
**`[info]`**
|
||||||
|
|
||||||
| Key | Default | Description |
|
| Key | Default | Description |
|
||||||
|---|---|---|
|
| --- | --- | --- |
|
||||||
| `METEO_DEFAULT_CITY` | _(empty)_ | Default city for `meteo` when no argument is given |
|
| `METEO_DEFAULT_CITY` | _(empty)_ | Default city for `meteo` when no argument is given |
|
||||||
|
|
||||||
**`[net]`**
|
**`[net]`**
|
||||||
|
|
||||||
| Key | Default | Description |
|
| Key | Default | Description |
|
||||||
|---|---|---|
|
| --- | --- | --- |
|
||||||
| `DWL_PREFERRED_TOOL` | _(empty)_ | Force `dwl` to use `curl`, `wget`, or `fetch` (auto-detected when unset) |
|
| `DWL_PREFERRED_TOOL` | _(empty)_ | Force `dwl` to use `curl`, `wget`, or `fetch` (auto-detected when unset) |
|
||||||
| `MYEXTIP_DEFAULT_URL` | `https://ip-api.com/json` | API endpoint used by `myextip` |
|
| `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]`**
|
**`[packages]`**
|
||||||
|
|
||||||
| Key | Default | Description |
|
| Key | Default | Description |
|
||||||
|---|---|---|
|
| --- | --- | --- |
|
||||||
| `PKGS_DEFAULT_IGNORE_CASE` | `0` | Set to `1` to make `pkgs` case-insensitive by default |
|
| `PKGS_DEFAULT_IGNORE_CASE` | `0` | Set to `1` to make `pkgs` case-insensitive by default |
|
||||||
|
|
||||||
**`[processes]`**
|
**`[processes]`**
|
||||||
|
|
||||||
| Key | Default | Description |
|
| Key | Default | Description |
|
||||||
|---|---|---|
|
| --- | --- | --- |
|
||||||
| `PPU_DEFAULT_FORMAT` | `pid,user,comm,args` | `ps` output format used by `ppu` |
|
| `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` |
|
| `KU_DEFAULT_SIGNAL` | `TERM` | Default signal sent by `ku` |
|
||||||
|
|
||||||
**`[updates]`**
|
**`[updates]`**
|
||||||
|
|
||||||
| Key | Default | Description |
|
| Key | Default | Description |
|
||||||
|---|---|---|
|
| --- | --- | --- |
|
||||||
| `UPDT_DEFAULT_BRANCH` | `master` | Git branch used for update checks and `profile_upgrade`. Changing this value 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` | 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
|
### 4.3. Locale shortcuts
|
||||||
|
|
||||||
@@ -244,6 +390,23 @@ PROMPT_THEME_DIR = ~/.mythemes # optional: custom search directory
|
|||||||
Built-in themes: `default`, `dark`, `light`, `solarized`, `solarized-light`,
|
Built-in themes: `default`, `dark`, `light`, `solarized`, `solarized-light`,
|
||||||
`monokai`, `monochrome`, `abyss`, `plasma`, `adwaita`.
|
`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:**
|
**Overriding individual prompt colour slots:**
|
||||||
|
|
||||||
```ini
|
```ini
|
||||||
@@ -252,10 +415,15 @@ PROMPT_COLOR_USER_FG = $ICyan
|
|||||||
PROMPT_COLOR_DIR_FG = $IYellow
|
PROMPT_COLOR_DIR_FG = $IYellow
|
||||||
```
|
```
|
||||||
|
|
||||||
The eleven available `PROMPT_COLOR_*` keys are:
|
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 |
|
| Key | Role |
|
||||||
|---|---|
|
| --- | --- |
|
||||||
| `PROMPT_COLOR_TIME_FG` / `TIME_BG` | Clock foreground / background |
|
| `PROMPT_COLOR_TIME_FG` / `TIME_BG` | Clock foreground / background |
|
||||||
| `PROMPT_COLOR_BAR_BG` | Main bar background |
|
| `PROMPT_COLOR_BAR_BG` | Main bar background |
|
||||||
| `PROMPT_COLOR_OK_FG` / `OK_MARK` | Exit-code text / checkmark on success |
|
| `PROMPT_COLOR_OK_FG` / `OK_MARK` | Exit-code text / checkmark on success |
|
||||||
@@ -263,6 +431,30 @@ The eleven available `PROMPT_COLOR_*` keys are:
|
|||||||
| `PROMPT_COLOR_ROOT_FG` | Username colour when running as root |
|
| `PROMPT_COLOR_ROOT_FG` | Username colour when running as root |
|
||||||
| `PROMPT_COLOR_USER_FG` | Username@host colour for normal users |
|
| `PROMPT_COLOR_USER_FG` | Username@host colour for normal users |
|
||||||
| `PROMPT_COLOR_DIR_FG` | Working directory colour |
|
| `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:**
|
**Writing a custom theme file:**
|
||||||
|
|
||||||
@@ -286,19 +478,57 @@ and other colour-aware tools.
|
|||||||
True-colour themes (`solarized`, `solarized-light`) require a terminal with
|
True-colour themes (`solarized`, `solarized-light`) require a terminal with
|
||||||
24-bit colour support (Konsole, iTerm2, kitty, Alacritty, Windows Terminal).
|
24-bit colour support (Konsole, iTerm2, kitty, Alacritty, Windows Terminal).
|
||||||
Verify support with:
|
Verify support with:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
printf '\e[38;2;38;139;210mTrue colour test\e[0m\n'
|
printf '\e[38;2;38;139;210mTrue colour test\e[0m\n'
|
||||||
```
|
```
|
||||||
|
|
||||||
## 5. Contact and more information
|
## 5. Optional dependencies
|
||||||
### 5.1. New users
|
|
||||||
|
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
|
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
|
let me know, especially if you don't plan to contribute. If you plan to
|
||||||
contribute, I'll be twice happier for sure!
|
contribute, I'll be twice happier for sure!
|
||||||
|
|
||||||
### 5.2. Bugs
|
### 6.2. Bugs
|
||||||
|
|
||||||
**profile** bug tracker is hosted on its Gitea instance. Check the
|
**profile** bug tracker is hosted on its Gitea instance. Check the
|
||||||
https://git.geoffray-levasseur.org/fatalerrors/profile page. If you find a bug,
|
<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
|
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
|
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
|
where the bug happens, OS details, the module that triggered it, if any, and the
|
||||||
@@ -310,7 +540,8 @@ 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
|
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.
|
sending any feature request or bug report, as it might already be documented.
|
||||||
|
|
||||||
### 5.3. How to contribute?
|
### 6.3. How to contribute?
|
||||||
|
|
||||||
You are free to improve and contribute as you wish. If you have no idea what to
|
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),
|
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
|
containing desired future improvements. Make sure you always have the latest
|
||||||
@@ -332,6 +563,7 @@ installation will probably be rejected.
|
|||||||
If you want to make a financial contribution, please contact me by mail.
|
If you want to make a financial contribution, please contact me by mail.
|
||||||
|
|
||||||
### 5.4. License, website, and maintainer
|
### 5.4. License, website, and maintainer
|
||||||
|
|
||||||
Everything except configuration files is licensed under the BSD-3 license.
|
Everything except configuration files is licensed under the BSD-3 license.
|
||||||
Please check the license file alongside this one.
|
Please check the license file alongside this one.
|
||||||
|
|
||||||
@@ -344,4 +576,4 @@ You can mail the author at fatalerrors \<at\> geoffray-levasseur \<dot\> org.
|
|||||||
Documentation (c) 2021-2026 Geoffray Levasseur.
|
Documentation (c) 2021-2026 Geoffray Levasseur.
|
||||||
|
|
||||||
This file is distributed under the 3-clause BSD license. The complete license
|
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
|
agreement can be obtained at: <https://opensource.org/licenses/BSD-3-Clause>
|
||||||
|
|||||||
Executable → Regular
+296
-6
@@ -7,9 +7,111 @@ 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
|
## [3.99.2-4_rc_2] — 2026-04-21
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
- **`prompt.sh`** — `\$Last_Command` in PS1 was escaped, preventing the exit
|
- **`prompt.sh`** — `\$Last_Command` in PS1 was escaped, preventing the exit
|
||||||
code from ever appearing in the prompt (the local variable no longer exists
|
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
|
when PS1 is rendered by bash). Removed the backslash so the value is embedded
|
||||||
@@ -24,6 +126,7 @@ Versions follow `MAJOR.MINOR.PATCH-REVISION_STAGE_N` (e.g. `3.99.1-4_rc_1`).
|
|||||||
`"Usage: ppg <string>"` instead of `"Usage: kt <pid>"`.
|
`"Usage: ppg <string>"` instead of `"Usage: kt <pid>"`.
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
- **`packages.sh` — `get_pkgmgr()`** — new exported helper that detects the
|
- **`packages.sh` — `get_pkgmgr()`** — new exported helper that detects the
|
||||||
active package manager of the running distribution. Detection first reads
|
active package manager of the running distribution. Detection first reads
|
||||||
`/etc/os-release` (`ID` then `ID_LIKE`), then falls back to a
|
`/etc/os-release` (`ID` then `ID_LIKE`), then falls back to a
|
||||||
@@ -36,6 +139,7 @@ Versions follow `MAJOR.MINOR.PATCH-REVISION_STAGE_N` (e.g. `3.99.1-4_rc_1`).
|
|||||||
## [3.99.1-4_rc_1] — 2026
|
## [3.99.1-4_rc_1] — 2026
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
- **Theming system** — `load_theme` in `profile.d/prompt.sh` loads `.theme`
|
- **Theming system** — `load_theme` in `profile.d/prompt.sh` loads `.theme`
|
||||||
files from `profile.d/themes/` (or a custom directory set via
|
files from `profile.d/themes/` (or a custom directory set via
|
||||||
`PROMPT_THEME_DIR`). Theme files are **parsed, not executed** — no shell code
|
`PROMPT_THEME_DIR`). Theme files are **parsed, not executed** — no shell code
|
||||||
@@ -63,6 +167,7 @@ Versions follow `MAJOR.MINOR.PATCH-REVISION_STAGE_N` (e.g. `3.99.1-4_rc_1`).
|
|||||||
never accidentally staged.
|
never accidentally staged.
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
- README §2 now explains how to create `profile.conf` from
|
- README §2 now explains how to create `profile.conf` from
|
||||||
`doc/profile.conf.example` (new section 2.1 "Initial configuration").
|
`doc/profile.conf.example` (new section 2.1 "Initial configuration").
|
||||||
- README §4 updated with full module-defaults tables, theming reference, and a
|
- README §4 updated with full module-defaults tables, theming reference, and a
|
||||||
@@ -71,6 +176,7 @@ Versions follow `MAJOR.MINOR.PATCH-REVISION_STAGE_N` (e.g. `3.99.1-4_rc_1`).
|
|||||||
variables, only data).
|
variables, only data).
|
||||||
|
|
||||||
### Security
|
### Security
|
||||||
|
|
||||||
- `load_theme` uses a strict allowlist (no `eval`, no sourcing). Only
|
- `load_theme` uses a strict allowlist (no `eval`, no sourcing). Only
|
||||||
`PROMPT_COLOR_*` keys and known `disp.sh` colour variable names are accepted.
|
`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
|
Values must match `\$[A-Za-z_][A-Za-z0-9_]*` or `\\e\[[0-9;]*m`; any other
|
||||||
@@ -78,15 +184,10 @@ Versions follow `MAJOR.MINOR.PATCH-REVISION_STAGE_N` (e.g. `3.99.1-4_rc_1`).
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
> **Note:** Versions prior to `3.95.x-4_beta` did not maintain a formal
|
|
||||||
> changelog. The full history of earlier changes is available through the git
|
|
||||||
> log (`git log --oneline`).
|
|
||||||
|
|
||||||
## [3.95.3-4_beta_3] — 2024
|
## [3.95.3-4_beta_3] — 2024
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
- Initial public release candidate series.
|
- Initial public release candidate series.
|
||||||
- Core modules: `compress`, `disp`, `filefct`, `fun`, `help`, `info`, `lang`,
|
- Core modules: `compress`, `disp`, `filefct`, `fun`, `help`, `info`, `lang`,
|
||||||
`net`, `packages`, `processes`, `prompt`, `pwd`, `rain`, `ssh`, `updates`.
|
`net`, `packages`, `processes`, `prompt`, `pwd`, `rain`, `ssh`, `updates`.
|
||||||
@@ -95,3 +196,192 @@ Versions follow `MAJOR.MINOR.PATCH-REVISION_STAGE_N` (e.g. `3.99.1-4_rc_1`).
|
|||||||
- `genpwd` / `pwdscore` password tools.
|
- `genpwd` / `pwdscore` password tools.
|
||||||
- `matrix` / `rain` screensavers.
|
- `matrix` / `rain` screensavers.
|
||||||
- `profile_upgrade` with git and archive download support.
|
- `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>
|
||||||
|
|||||||
Executable → Regular
+54
-5
@@ -39,6 +39,7 @@ to target). Stale forks cause avoidable merge conflicts.
|
|||||||
**New functionality** must always target `master`.
|
**New functionality** must always target `master`.
|
||||||
|
|
||||||
**Bugfixes** must target the branch where the bug was introduced:
|
**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
|
- If the bug exists in a released version, open the fix against that version's
|
||||||
maintenance branch first, then cherry-pick onto `master`.
|
maintenance branch first, then cherry-pick onto `master`.
|
||||||
- If the bug is only in `master` (unreleased), fix it directly on `master`.
|
- If the bug is only in `master` (unreleased), fix it directly on `master`.
|
||||||
@@ -71,13 +72,15 @@ Any experimental version must have it's dedicated branch.
|
|||||||
---
|
---
|
||||||
|
|
||||||
## 5. Development environment
|
## 5. Development environment
|
||||||
|---|---|---|
|
|
||||||
|
| --- | --- | --- |
|
||||||
| Bash | 4.3 | Namerefs (`local -n`) required |
|
| Bash | 4.3 | Namerefs (`local -n`) required |
|
||||||
| shellcheck | any recent | Run before every commit |
|
| shellcheck | any recent | Run before every commit |
|
||||||
| git | any | For contributing patches |
|
| git | any | For contributing patches |
|
||||||
| bats-core | 1.x | Optional — for running the test suite |
|
| bats-core | 1.x | Optional — for running the test suite |
|
||||||
|
|
||||||
Install shellcheck:
|
Install shellcheck:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Debian / Ubuntu
|
# Debian / Ubuntu
|
||||||
apt-get install shellcheck
|
apt-get install shellcheck
|
||||||
@@ -94,6 +97,7 @@ brew install shellcheck
|
|||||||
## 6. Code style
|
## 6. Code style
|
||||||
|
|
||||||
### General rules
|
### General rules
|
||||||
|
|
||||||
- **Bash only** — no external interpreters in core modules. Python or Perl is
|
- **Bash only** — no external interpreters in core modules. Python or Perl is
|
||||||
acceptable for completely self-contained, optional utilities that have no
|
acceptable for completely self-contained, optional utilities that have no
|
||||||
dependencies beyond a minimal Debian or CentOS installation.
|
dependencies beyond a minimal Debian or CentOS installation.
|
||||||
@@ -104,22 +108,37 @@ brew install shellcheck
|
|||||||
environment. Prefer upper case for global and lowercase for local.
|
environment. Prefer upper case for global and lowercase for local.
|
||||||
- **`printf`** instead of `echo` all the time.
|
- **`printf`** instead of `echo` all the time.
|
||||||
- **Never `eval`** — use namerefs (`local -n`), `${!varname}` indirection, or
|
- **Never `eval`** — use namerefs (`local -n`), `${!varname}` indirection, or
|
||||||
`declare -g` instead.
|
`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
|
- **No hardcoded defaults** — wire every configurable value through
|
||||||
`${VAR:-default}` and document the key in `profile.conf` and `README.md §4`.
|
`${VAR:-default}` and document the key in `profile.conf` and `README.md §4`.
|
||||||
|
|
||||||
### Function conventions
|
### Function conventions
|
||||||
|
|
||||||
- Public functions **must** be exported: `export -f funcname`.
|
- Public functions **must** be exported: `export -f funcname`.
|
||||||
- Every public function **must** support `-h` / `--help` and print usage to
|
- Every public function **must** support `-h` / `--help` and print usage to
|
||||||
stdout, returning 0.
|
stdout, returning 0.
|
||||||
- Use `getopt` (not `getopts`) for option parsing — it handles long options and
|
- Use `getopt` (not `getopts`) for option parsing — it handles long options and
|
||||||
`--` correctly.
|
`--` 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,
|
- Follow existing error-return conventions: 0 = success, 1 = usage error,
|
||||||
2 = bad options, 3 = missing dependency, 4+ = runtime failure.
|
2 = bad options, 3 = missing dependency, 4+ = runtime failure.
|
||||||
- Prefix all local helper variables with a short unique prefix (e.g. `_taz_`)
|
- Prefix all local helper variables with a short unique prefix (e.g. `_taz_`)
|
||||||
to prevent collisions with caller-scope variables.
|
to prevent collisions with caller-scope variables.
|
||||||
|
|
||||||
### Module structure
|
### Module structure
|
||||||
|
|
||||||
Every new module should follow this pattern:
|
Every new module should follow this pattern:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -137,6 +156,29 @@ export -f my_function
|
|||||||
|
|
||||||
Add the `load_conf` call near the top after any variable declarations.
|
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
|
## 7. Configuration keys
|
||||||
@@ -160,10 +202,12 @@ See the existing themes and `README.md §4.4` for the allowed syntax.
|
|||||||
## 9. Running shellcheck
|
## 9. Running shellcheck
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
shellcheck -x profile.sh profile.d/*.sh
|
shellcheck profile.sh profile.d/*.sh helpers/*.sh profile.d/bash-completion/*.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
All warnings must be resolved before a patch will be accepted. Accepted
|
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
|
suppression directives (`# shellcheck disable=SCxxxx`) require an inline
|
||||||
comment explaining why the suppression is necessary.
|
comment explaining why the suppression is necessary.
|
||||||
|
|
||||||
@@ -172,20 +216,25 @@ comment explaining why the suppression is necessary.
|
|||||||
## 10. Submitting a contribution
|
## 10. Submitting a contribution
|
||||||
|
|
||||||
### Via Git (preferred)
|
### Via Git (preferred)
|
||||||
|
|
||||||
1. Contact the maintainer to obtain push access, or fork on the Gitea instance.
|
1. Contact the maintainer to obtain push access, or fork on the Gitea instance.
|
||||||
2. Create a branch: `git checkout -b feature/my-feature`.
|
2. Create a branch: `git checkout -b feature/my-feature`.
|
||||||
3. Commit with a clear subject line: `module: short description (≤ 72 chars)`.
|
3. Commit with a clear subject line: `module: short description (≤ 72 chars)`.
|
||||||
4. Push and open a pull request against `master`.
|
4. Push and open a pull request against `master`.
|
||||||
|
|
||||||
### Via patch
|
### Via patch
|
||||||
|
|
||||||
If you do not have push access:
|
If you do not have push access:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git format-patch origin/master
|
git format-patch origin/master
|
||||||
```
|
```
|
||||||
|
|
||||||
Send the resulting `.patch` file(s) to
|
Send the resulting `.patch` file(s) to
|
||||||
`fatalerrors <at> geoffray-levasseur <dot> org`.
|
`fatalerrors <at> geoffray-levasseur <dot> org`.
|
||||||
|
|
||||||
### Commit message format
|
### Commit message format
|
||||||
|
|
||||||
```
|
```
|
||||||
module: imperative short description
|
module: imperative short description
|
||||||
|
|
||||||
@@ -197,7 +246,7 @@ Reference issue numbers if applicable: closes #42.
|
|||||||
|
|
||||||
## 11. What will be rejected
|
## 11. What will be rejected
|
||||||
|
|
||||||
- Code requiring packages not in a minimal Debian or CentOS install.
|
- 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.
|
- Use of `eval`, `source`-based config loading, or other code-injection vectors.
|
||||||
- Changes that break Bash 4.3 compatibility.
|
- Changes that break Bash 4.3 compatibility.
|
||||||
- Patches without a passing `shellcheck` run.
|
- Patches without a passing `shellcheck` run.
|
||||||
|
|||||||
Executable → Regular
+228
-5
@@ -4,15 +4,50 @@
|
|||||||
|
|
||||||
## Installation & loading
|
## 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."**
|
**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
|
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 3.x for licensing reasons). Install a newer Bash:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# macOS
|
# macOS
|
||||||
brew install bash
|
brew install bash
|
||||||
# then add /opt/homebrew/bin/bash to /etc/shells and chsh
|
# then add /opt/homebrew/bin/bash to /etc/shells and chsh
|
||||||
```
|
```
|
||||||
|
|
||||||
Or point your terminal emulator at the newer binary explicitly.
|
Or point your terminal emulator at the newer binary explicitly.
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -25,13 +60,46 @@ 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.**
|
**Q: I set `PROFILE_PATH` but profile still can't find its modules.**
|
||||||
|
|
||||||
`PROFILE_PATH` must be exported *before* you source `profile.sh`:
|
`PROFILE_PATH` must be exported *before* you source `profile.sh`:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
export PROFILE_PATH=/opt/profile
|
export PROFILE_PATH=/opt/profile
|
||||||
source /opt/profile/profile.sh
|
source /opt/profile/profile.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
If set after sourcing, `MYPATH` is already locked in and the variable has
|
If set after sourcing, `MYPATH` is already locked in and the variable has
|
||||||
no effect.
|
no effect.
|
||||||
|
|
||||||
@@ -65,6 +133,7 @@ See also `README.md §4.2` for a consolidated table.
|
|||||||
**Q: A key I set in `profile.conf` is being ignored.**
|
**Q: A key I set in `profile.conf` is being ignored.**
|
||||||
|
|
||||||
Check that:
|
Check that:
|
||||||
|
|
||||||
1. The key is inside the correct `[section]` header.
|
1. The key is inside the correct `[section]` header.
|
||||||
2. There is no leading space before the section name (`[section]` not
|
2. There is no leading space before the section name (`[section]` not
|
||||||
`[ section ]`).
|
`[ section ]`).
|
||||||
@@ -74,17 +143,43 @@ Check that:
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
**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
|
## Prompt & theming
|
||||||
|
|
||||||
**Q: How do I change the prompt theme?**
|
**Q: How do I change the prompt theme?**
|
||||||
|
|
||||||
Add to `profile.conf`:
|
Add to `profile.conf`:
|
||||||
|
|
||||||
```ini
|
```ini
|
||||||
[prompt]
|
[prompt]
|
||||||
PROMPT_THEME = dark
|
PROMPT_THEME = dark
|
||||||
```
|
```
|
||||||
|
|
||||||
Built-in names: `default`, `dark`, `light`, `solarized`, `solarized-light`,
|
Built-in names: `default`, `dark`, `light`, `solarized`, `solarized-light`,
|
||||||
`monokai`, `monochrome`, `abyss`, `plasma`, `adwaita`.
|
`monokai`, `monochrome`, `abyss`, `plasma`, `adwaita`, but you can create your
|
||||||
|
own theme.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -92,16 +187,45 @@ Built-in names: `default`, `dark`, `light`, `solarized`, `solarized-light`,
|
|||||||
|
|
||||||
Those themes use 24-bit / true-colour ANSI sequences (`\e[38;2;R;G;Bm`).
|
Those themes use 24-bit / true-colour ANSI sequences (`\e[38;2;R;G;Bm`).
|
||||||
Test your terminal:
|
Test your terminal:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
printf '\e[38;2;38;139;210mTrue colour test\e[0m\n'
|
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 a solid blue word your terminal supports true colour.
|
||||||
If you see garbage or plain text, switch to a 16-colour theme
|
If you see garbage or plain text, switch to a 16-colour theme
|
||||||
(`dark`, `default`, etc.) or upgrade your terminal emulator.
|
(`dark`, `default`, etc.) or upgrade your terminal emulator.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
**Q: I created a custom theme but `load_theme` emits "key not allowed" warnings.**
|
**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
|
Theme files are parsed, not executed. Only `PROMPT_COLOR_*` keys and the
|
||||||
standard colour variable names from `disp.sh` (`Black`, `Blue`, `On_IBlack`,
|
standard colour variable names from `disp.sh` (`Black`, `Blue`, `On_IBlack`,
|
||||||
@@ -119,12 +243,66 @@ theme file cannot execute code. Values must be a colour variable reference
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## 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
|
## Functions
|
||||||
|
|
||||||
**Q: `meteo` prints "No city specified" even though I set a default.**
|
**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
|
The key is `METEO_DEFAULT_CITY` (not `DEFAULT_CITY`), and it must be in the
|
||||||
`[info]` section:
|
`[info]` section:
|
||||||
|
|
||||||
```ini
|
```ini
|
||||||
[info]
|
[info]
|
||||||
METEO_DEFAULT_CITY = Paris
|
METEO_DEFAULT_CITY = Paris
|
||||||
@@ -136,6 +314,7 @@ METEO_DEFAULT_CITY = Paris
|
|||||||
|
|
||||||
`dwl` requires one of `curl`, `wget`, or `fetch` to be installed.
|
`dwl` requires one of `curl`, `wget`, or `fetch` to be installed.
|
||||||
Install curl:
|
Install curl:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Debian / Ubuntu
|
# Debian / Ubuntu
|
||||||
apt-get install curl
|
apt-get install curl
|
||||||
@@ -143,15 +322,55 @@ apt-get install curl
|
|||||||
# Fedora / RHEL
|
# Fedora / RHEL
|
||||||
dnf install curl
|
dnf install curl
|
||||||
```
|
```
|
||||||
|
|
||||||
Or set `DWL_PREFERRED_TOOL` in `[net]` to whichever tool you have.
|
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.**
|
**Q: `pkgs` does not find packages I know are installed.**
|
||||||
|
|
||||||
`pkgs` delegates to `dpkg -l` (Debian/Ubuntu) or `rpm -qa` (RHEL/Fedora).
|
`pkgs` uses `get_pkgmgr` to detect the active package manager and delegates
|
||||||
If your distribution uses a different package manager (pacman, apk, brew …)
|
to the appropriate tool. Supported families: `apt` (Debian/Ubuntu),
|
||||||
it is not yet supported. See `doc/todo.md` for the tracking issue.
|
`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`.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -160,9 +379,11 @@ it is not yet supported. See `doc/todo.md` for the tracking issue.
|
|||||||
`check_updates` compares the content of the remote `version` file against
|
`check_updates` compares the content of the remote `version` file against
|
||||||
`$PROFVERSION`. If `UPDT_DEFAULT_BRANCH` in `[updates]` points to a different
|
`$PROFVERSION`. If `UPDT_DEFAULT_BRANCH` in `[updates]` points to a different
|
||||||
branch than your installation, the version files may not match. Check:
|
branch than your installation, the version files may not match. Check:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cat "$MYPATH/version"
|
cat "$MYPATH/version"
|
||||||
```
|
```
|
||||||
|
|
||||||
and make sure `UPDT_DEFAULT_BRANCH` matches the branch you track.
|
and make sure `UPDT_DEFAULT_BRANCH` matches the branch you track.
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -193,6 +414,7 @@ has not been implemented yet.
|
|||||||
```bash
|
```bash
|
||||||
PROFILE_DISABLED=1 bash --norc
|
PROFILE_DISABLED=1 bash --norc
|
||||||
```
|
```
|
||||||
|
|
||||||
Or simply open a shell without sourcing `~/.bashrc` (`bash --norc`).
|
Or simply open a shell without sourcing `~/.bashrc` (`bash --norc`).
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -202,6 +424,7 @@ Or simply open a shell without sourcing `~/.bashrc` (`bash --norc`).
|
|||||||
Open an issue on the
|
Open an issue on the
|
||||||
[Gitea tracker](https://git.geoffray-levasseur.org/fatalerrors/profile/issues)
|
[Gitea tracker](https://git.geoffray-levasseur.org/fatalerrors/profile/issues)
|
||||||
or send a mail to `fatalerrors <at> geoffray-levasseur <dot> org` with:
|
or send a mail to `fatalerrors <at> geoffray-levasseur <dot> org` with:
|
||||||
|
|
||||||
- The exact command that triggered the bug
|
- The exact command that triggered the bug
|
||||||
- Your OS and Bash version (`bash --version`)
|
- Your OS and Bash version (`bash --version`)
|
||||||
- The module involved
|
- The module involved
|
||||||
|
|||||||
Executable → Regular
@@ -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.
|
||||||
Executable → Regular
+93
-9
@@ -21,8 +21,10 @@ HISTIGNORE="&:[bf]g:exit"
|
|||||||
# Default pager
|
# Default pager
|
||||||
PAGER=less
|
PAGER=less
|
||||||
|
|
||||||
# Terminal colour capability
|
# Terminal type.
|
||||||
TERM=xterm-256color
|
# 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]
|
[compress]
|
||||||
@@ -30,8 +32,10 @@ TERM=xterm-256color
|
|||||||
# Supported: lz (default), xz, bz2, gz, lzo, tar, zip, zst
|
# Supported: lz (default), xz, bz2, gz, lzo, tar, zip, zst
|
||||||
#TAZ_DEFAULT_FORMAT=lz
|
#TAZ_DEFAULT_FORMAT=lz
|
||||||
|
|
||||||
# taz: Number of compression threads (0 = auto-detect CPU count).
|
# taz: Number of compression threads.
|
||||||
#TAZ_DEFAULT_THREADS=0
|
# 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: Compression level 1 (fast/large) … 9 (slow/small).
|
||||||
#TAZ_DEFAULT_LEVEL=6
|
#TAZ_DEFAULT_LEVEL=6
|
||||||
@@ -45,13 +49,23 @@ TERM=xterm-256color
|
|||||||
# never — always flatten into the current directory
|
# never — always flatten into the current directory
|
||||||
#UTAZ_DEFAULT_DIR_MODE=auto
|
#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]
|
[debug]
|
||||||
# (no configurable keys yet)
|
# 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]
|
[disp]
|
||||||
# Uncomment to disable ANSI colours in profile's own output messages.
|
# 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
|
#NO_COLOR=1
|
||||||
|
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
@@ -76,6 +90,36 @@ TERM=xterm-256color
|
|||||||
# busy: Delay between matched lines in milliseconds (0 = no delay).
|
# busy: Delay between matched lines in milliseconds (0 = no delay).
|
||||||
#BUSY_DEFAULT_DELAY=0
|
#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]
|
[info]
|
||||||
# meteo: Default city when no argument is given. Leave unset to require an
|
# meteo: Default city when no argument is given. Leave unset to require an
|
||||||
@@ -100,9 +144,15 @@ TERM=xterm-256color
|
|||||||
# Unset = auto-detect (curl preferred, then wget, then fetch).
|
# Unset = auto-detect (curl preferred, then wget, then fetch).
|
||||||
#DWL_PREFERRED_TOOL=curl
|
#DWL_PREFERRED_TOOL=curl
|
||||||
|
|
||||||
# myextip: API endpoint for external IP lookup.
|
# dwl: Enable resume mode by default (supports curl/wget file downloads).
|
||||||
# Alternatives: https://ipinfo.io/json, https://ip-api.com/json/
|
# Values accepted: 1/0, true/false, yes/no, on/off.
|
||||||
#MYEXTIP_DEFAULT_URL=https://ip-api.com/json/
|
#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]
|
[packages]
|
||||||
@@ -154,6 +204,28 @@ TERM=xterm-256color
|
|||||||
#
|
#
|
||||||
# Working directory
|
# Working directory
|
||||||
#PROMPT_COLOR_DIR_FG=$ICyan
|
#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]
|
[pwd]
|
||||||
@@ -190,12 +262,24 @@ TERM=xterm-256color
|
|||||||
# rain: Colour theme. Supported: white (default), green, blue, red, yellow, cyan
|
# rain: Colour theme. Supported: white (default), green, blue, red, yellow, cyan
|
||||||
#RAIN_DEFAULT_COLOR=white
|
#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: Falling speed.
|
||||||
#MATRIX_DEFAULT_SPEED=3.5
|
#MATRIX_DEFAULT_SPEED=3.5
|
||||||
|
|
||||||
# matrix: Colour theme. Supported: green (default), blue, red, yellow, cyan, white
|
# matrix: Colour theme. Supported: green (default), blue, red, yellow, cyan, white
|
||||||
#MATRIX_DEFAULT_COLOR=green
|
#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: Character set. Supported: binary (default), kana, ascii
|
||||||
#MATRIX_DEFAULT_CHARSET=binary
|
#MATRIX_DEFAULT_CHARSET=binary
|
||||||
|
|
||||||
|
|||||||
Executable → Regular
+27
-5
@@ -36,8 +36,18 @@ TERM=xterm-256color
|
|||||||
# never — always flatten extraction into the current directory
|
# never — always flatten extraction into the current directory
|
||||||
#UTAZ_DEFAULT_DIR_MODE=auto
|
#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]
|
[debug]
|
||||||
# Section used by debug.sh (nothing yet)
|
# 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]
|
[disp]
|
||||||
# Section used by disp.sh
|
# Section used by disp.sh
|
||||||
@@ -91,10 +101,14 @@ SET_LOCALE="fr:fr_FR.UTF-8,us:en_US.UTF-8"
|
|||||||
# Supported values: curl, wget, fetch. Unset uses auto-detection (default).
|
# Supported values: curl, wget, fetch. Unset uses auto-detection (default).
|
||||||
#DWL_PREFERRED_TOOL=curl
|
#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.
|
# myextip: API endpoint URL used to retrieve external IP information.
|
||||||
# Default: https://ip-api.com/json/
|
# Default: https://ipinfo.io/json
|
||||||
# Compatible alternatives: https://ipinfo.io/json, https://ip-api.com/json/
|
# Compatible alternative: https://ipwho.is/ (ip-api.com is HTTP only on its free tier).
|
||||||
#MYEXTIP_DEFAULT_URL=https://ip-api.com/json/
|
#MYEXTIP_DEFAULT_URL=https://ipinfo.io/json
|
||||||
|
|
||||||
[packages]
|
[packages]
|
||||||
# Section used by packages.sh
|
# Section used by packages.sh
|
||||||
@@ -191,6 +205,10 @@ SET_LOCALE="fr:fr_FR.UTF-8,us:en_US.UTF-8"
|
|||||||
# Supported values: white (default), green, blue, red, yellow, cyan
|
# Supported values: white (default), green, blue, red, yellow, cyan
|
||||||
#RAIN_DEFAULT_COLOR=white
|
#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 value, using the /100 scale (3.5 => 0.035s).
|
||||||
#MATRIX_DEFAULT_SPEED=3.5
|
#MATRIX_DEFAULT_SPEED=3.5
|
||||||
|
|
||||||
@@ -198,6 +216,10 @@ SET_LOCALE="fr:fr_FR.UTF-8,us:en_US.UTF-8"
|
|||||||
# Supported values: green (default), blue, red, yellow, cyan, white
|
# Supported values: green (default), blue, red, yellow, cyan, white
|
||||||
#MATRIX_DEFAULT_COLOR=green
|
#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.
|
# matrix: Default character set.
|
||||||
# Supported values: binary (default), kana, ascii
|
# Supported values: binary (default), kana, ascii
|
||||||
MATRIX_DEFAULT_CHARSET=kana
|
MATRIX_DEFAULT_CHARSET=kana
|
||||||
@@ -260,7 +282,7 @@ mkdin='make DESTDIR=$PWD/dest-install install'
|
|||||||
ssh='ssh -Y'
|
ssh='ssh -Y'
|
||||||
|
|
||||||
# Resume mode for wget
|
# Resume mode for wget
|
||||||
wget='wget -c' # resume mode by default
|
wget='wget -c'
|
||||||
|
|
||||||
# Human readable by default
|
# Human readable by default
|
||||||
df='df -H'
|
df='df -H'
|
||||||
|
|||||||
Executable → Regular
+75
-15
@@ -12,7 +12,7 @@ version-bump.
|
|||||||
blockers are `local -A` (no associative arrays in ZSH without `typeset -A`)
|
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
|
and `local -n` namerefs. A thin compatibility shim would open the project to
|
||||||
ZSH users. **[hard]**
|
ZSH users. **[hard]**
|
||||||
- [ ] **Bash completion** — add a `profile.d/completion/` directory and write
|
- [ ] **Bash completion** — add a more bash completion directory and write
|
||||||
`_profile_upgrade`, `_taz`, `_utaz`, `_meteo`, etc. completions so that
|
`_profile_upgrade`, `_taz`, `_utaz`, `_meteo`, etc. completions so that
|
||||||
`<Tab>` works on all public functions. **[medium]**
|
`<Tab>` works on all public functions. **[medium]**
|
||||||
|
|
||||||
@@ -20,18 +20,21 @@ version-bump.
|
|||||||
|
|
||||||
## Prompt & theming
|
## Prompt & theming
|
||||||
|
|
||||||
- [ ] **Git branch in prompt** — show the current branch name (and dirty
|
- [x] **Git branch in prompt** — show the current branch name (with dirty and
|
||||||
indicator) in the PS1 bar when inside a Git repository. Should be
|
upstream drift indicators) in the PS1 bar when inside a Git repository,
|
||||||
gated behind a `[prompt]` config key so it can be disabled. **[medium]**
|
gated by `[prompt]` config keys. **[medium]**
|
||||||
- [ ] **Virtual-env / conda indicator** — detect `$VIRTUAL_ENV` / `$CONDA_DEFAULT_ENV`
|
- [x] **Virtual-env / conda indicator** — detect `$VIRTUAL_ENV` / `$CONDA_DEFAULT_ENV`
|
||||||
and display the name in the prompt bar. **[easy]**
|
and display the active environment in the prompt bar. **[easy]**
|
||||||
- [ ] **True-colour terminal auto-detection** — query `$COLORTERM` and
|
- [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
|
`$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]**
|
16-colour equivalent when the terminal does not support true colour. **[medium]**
|
||||||
- [ ] **True-colour variants of other themes** — create `monokai-tc.theme`,
|
- [ ] **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
|
`abyss-tc.theme`, etc. using the same `\e[38;2;R;G;Bm` approach as the
|
||||||
Solarized themes. **[easy]** _(per theme)_
|
Solarized themes. **[easy]** _(per theme)_
|
||||||
- [ ] **Theme preview command** — add a `theme_preview` (or `profile_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
|
function that renders a colour swatch and a sample prompt line for the
|
||||||
currently loaded theme, so users can evaluate themes without reloading
|
currently loaded theme, so users can evaluate themes without reloading
|
||||||
the session. **[medium]**
|
the session. **[medium]**
|
||||||
@@ -44,46 +47,54 @@ version-bump.
|
|||||||
## Module improvements
|
## Module improvements
|
||||||
|
|
||||||
### compress
|
### compress
|
||||||
|
|
||||||
- [ ] **`taz` progress bar** — show a `pv` / `dd`-based progress indicator when
|
- [ ] **`taz` progress bar** — show a `pv` / `dd`-based progress indicator when
|
||||||
compressing large trees, gated behind a `-p` flag. **[medium]**
|
compressing large trees, gated behind a `-p` flag. **[medium]**
|
||||||
- [ ] **`utaz` integrity check** — run `tar -tOf` / `unzip -t` / `7z t` before
|
- [ ] **`utaz` integrity check** — run `tar -tOf` / `unzip -t` / `7z t` before
|
||||||
extracting and abort if the archive is corrupt. **[easy]**
|
extracting and abort if the archive is corrupt. **[easy]**
|
||||||
|
|
||||||
### filefct
|
### filefct
|
||||||
- [ ] **`findbig` / `findzero` / `finddead` — `fd` integration** — optionally
|
|
||||||
|
- [x] **`findbig` / `findzero` / `finddead` — `fd` integration** — optionally
|
||||||
use `fd` instead of `find` when available for faster traversal. **[easy]**
|
use `fd` instead of `find` when available for faster traversal. **[easy]**
|
||||||
- [ ] **`file_stats` — human-readable totals** — add `--human` flag to emit
|
- [x] **`file_stats` — human-readable totals** — add `--human` flag to emit
|
||||||
sizes in K/M/G instead of bytes. **[easy]**
|
sizes in K/M/G instead of bytes. **[easy]**
|
||||||
|
|
||||||
### info
|
### info
|
||||||
- [ ] **`showinfo` fallback** — when neither `neofetch` nor `fastfetch` is
|
|
||||||
|
- [X] **`showinfo` fallback** — when neither `neofetch` nor `fastfetch` is
|
||||||
installed, print a minimal sysinfo block (hostname, OS, kernel, uptime,
|
installed, print a minimal sysinfo block (hostname, OS, kernel, uptime,
|
||||||
CPU, RAM) using pure Bash + `/proc`. **[medium]**
|
CPU, RAM) using pure Bash + `/proc`. **[medium]**
|
||||||
|
|
||||||
### net
|
### net
|
||||||
- [ ] **`dwl` resume support** — pass `-C -` to curl / `--continue-at -` to
|
|
||||||
|
- [X] **`dwl` resume support** — pass `-C -` to curl / `--continue-at -` to
|
||||||
wget for interrupted downloads; gate behind a `-r` flag. **[easy]**
|
wget for interrupted downloads; gate behind a `-r` flag. **[easy]**
|
||||||
- [ ] **`myextip` multiple providers** — fall back to a secondary URL
|
- [ ] **`myextip` multiple providers** — fall back to a secondary URL
|
||||||
(configurable via `MYEXTIP_FALLBACK_URL`) when the primary times out.
|
(configurable via `MYEXTIP_FALLBACK_URL`) when the primary times out.
|
||||||
**[easy]**
|
**[easy]**
|
||||||
|
|
||||||
### processes
|
### processes
|
||||||
- [ ] **`ku` dry-run flag** — add `-n` / `--dry-run` to print what would be
|
|
||||||
|
- [X] **`ku` dry-run flag** — add `-n` / `--dry-run` to print what would be
|
||||||
killed without acting. **[easy]**
|
killed without acting. **[easy]**
|
||||||
|
|
||||||
### pwd
|
### pwd
|
||||||
|
|
||||||
- [ ] **`genpwd` passphrase mode** — add `-w` / `--words N` to generate
|
- [ ] **`genpwd` passphrase mode** — add `-w` / `--words N` to generate
|
||||||
word-based passphrases (diceware-style) from `/usr/share/dict/words`.
|
word-based passphrases (diceware-style) from `/usr/share/dict/words`.
|
||||||
**[medium]**
|
**[medium]**
|
||||||
|
|
||||||
### ssh
|
### ssh
|
||||||
|
|
||||||
- [ ] **SSH agent management** — add `ssh_agent_start` / `ssh_agent_stop` helpers
|
- [ ] **SSH agent management** — add `ssh_agent_start` / `ssh_agent_stop` helpers
|
||||||
that start a persistent `ssh-agent`, add configured keys, and survive
|
that start a persistent `ssh-agent`, add configured keys, and survive
|
||||||
re-login via a socket stored in `~/.ssh/agent.env`. **[medium]**
|
re-login via a socket stored in `~/.ssh/agent.env`. **[medium]**
|
||||||
- [ ] **`rmhost` glob support** — allow `rmhost '*.example.com'` to remove all
|
- [x] **`rmhost` glob support** — allow `rmhost '*.example.com'` to remove all
|
||||||
matching entries in one call. **[easy]**
|
matching entries in one call. **[easy]**
|
||||||
|
|
||||||
### updates
|
### updates
|
||||||
|
|
||||||
- [ ] **Automatic update check age** — store a timestamp in `~/.cache/profile_last_check`;
|
- [ ] **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
|
skip the network request in `check_updates -q` if the last check was less
|
||||||
than `UPDT_CHECK_INTERVAL` hours ago (configurable, default 24). **[medium]**
|
than `UPDT_CHECK_INTERVAL` hours ago (configurable, default 24). **[medium]**
|
||||||
@@ -93,6 +104,55 @@ version-bump.
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## 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
|
## Infrastructure
|
||||||
|
|
||||||
- [ ] **Test suite** — add a `test/` directory with `bats` (Bash Automated
|
- [ ] **Test suite** — add a `test/` directory with `bats` (Bash Automated
|
||||||
@@ -107,7 +167,7 @@ version-bump.
|
|||||||
- [ ] **`profile_uninstall` function** — remove the `source` line from
|
- [ ] **`profile_uninstall` function** — remove the `source` line from
|
||||||
`~/.bashrc` / `~/.profile` and optionally delete the install directory,
|
`~/.bashrc` / `~/.profile` and optionally delete the install directory,
|
||||||
with a dry-run mode. **[medium]**
|
with a dry-run mode. **[medium]**
|
||||||
- [ ] **`disp` syslog integration** — add a `DISP_SYSLOG=1` config key that
|
- [ ] **`disp` syslog integration** — add a `DISP_SYSLOG=<context>` config key that
|
||||||
additionally pipes E/W messages to `logger`. **[easy]**
|
additionally pipes E/W messages to `logger`. **[easy]**
|
||||||
- [ ] **XDG base-dir support** — honour `$XDG_CONFIG_HOME` as an alternative
|
- [ ] **XDG base-dir support** — honour `$XDG_CONFIG_HOME` as an alternative
|
||||||
location for `profile.conf` so users can keep `~` tidy. **[medium]**
|
location for `profile.conf` so users can keep `~` tidy. **[medium]**
|
||||||
|
|||||||
@@ -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" ]]
|
||||||
|
}
|
||||||
-147
@@ -1,147 +0,0 @@
|
|||||||
------------------------------------------------------------------------------
|
|
||||||
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>
|
|
||||||
------------------------------------------------------------------------------
|
|
||||||
Version history:
|
|
||||||
------------------------------------------------------------------------------
|
|
||||||
# 05/03/2026 v3.6.1
|
|
||||||
Fix a typo in compress.sh
|
|
||||||
|
|
||||||
# 05/03/2026 v3.6.0
|
|
||||||
Improved utaz to make it multiformat with lot of it
|
|
||||||
Introduced ppu and ppn
|
|
||||||
Improved update system
|
|
||||||
|
|
||||||
# 04/03/2026 v3.5.0
|
|
||||||
rain has now configurable speed and color
|
|
||||||
showinfo adapted to fastfetch, replacing neofetch
|
|
||||||
|
|
||||||
# 24/02/2022 v3.3.1
|
|
||||||
Fixed version detection
|
|
||||||
Added "busy" function
|
|
||||||
Fixed use of library functions before it's loaded
|
|
||||||
|
|
||||||
# 28/11/2022 v3.3.0
|
|
||||||
Initial version update support
|
|
||||||
Changed versioning code
|
|
||||||
Added installation path detection
|
|
||||||
|
|
||||||
# 28/11/2022 v3.2.3
|
|
||||||
Made proper readme file, to improve
|
|
||||||
|
|
||||||
# 21/11/2022 v3.2.2
|
|
||||||
Fixed taz compression level analysis
|
|
||||||
Fixed typo in dpkgs
|
|
||||||
|
|
||||||
# 20/11/2022 v3.2.1
|
|
||||||
Fix some messages
|
|
||||||
Make dpkgs rpm aware (more to come)
|
|
||||||
Removed version history from main script and revert declaration order
|
|
||||||
Added required license information in all files
|
|
||||||
Completed LICENSE file
|
|
||||||
|
|
||||||
# 18/11/2022 v3.2.0
|
|
||||||
Created disp command for display and make use of it
|
|
||||||
|
|
||||||
# 10/11/2022 v3.1.1
|
|
||||||
genpwd: test if password is doable
|
|
||||||
|
|
||||||
# 08/11/2022 v3.1.0
|
|
||||||
Added password generator
|
|
||||||
|
|
||||||
# 07/11/2022 v3.0.1
|
|
||||||
Added concatenation to rmspc
|
|
||||||
Added ku
|
|
||||||
Error managed in meteo
|
|
||||||
|
|
||||||
# 27/08/2022 v3.0.0
|
|
||||||
Splitted everything in several files
|
|
||||||
Added rain screensaver
|
|
||||||
|
|
||||||
# 29/07/2022 v2.8.2
|
|
||||||
Added warning for non bash or zsh users
|
|
||||||
|
|
||||||
# 19/07/2022 v2.8.1
|
|
||||||
Few cleanups, fixes and optimizations
|
|
||||||
|
|
||||||
# 24/06/2022 v2.8.0
|
|
||||||
Added backtrace, error and settrace
|
|
||||||
[bugfix] corrected showinfo
|
|
||||||
|
|
||||||
# 22/06/2022 v2.7.1
|
|
||||||
[bugfix] few minor corrections
|
|
||||||
Added help command
|
|
||||||
|
|
||||||
# 21/06/2022 v2.7.0
|
|
||||||
Added isipv4 and isipv6 and use it in rmhost as an improvement
|
|
||||||
Removed konsole save and restore not working
|
|
||||||
|
|
||||||
# 18/10/2021 v2.6.3
|
|
||||||
Changed PS1 for status bar style version
|
|
||||||
Few minor improvements
|
|
||||||
|
|
||||||
# 26/02/2021 v2.6.2
|
|
||||||
[bugfix] taz: corrected bug with trailing slash on directories
|
|
||||||
|
|
||||||
# 25/12/2020 v2.6.1
|
|
||||||
Add check on rmhost
|
|
||||||
Improvements rmspc
|
|
||||||
Created expendlist
|
|
||||||
|
|
||||||
# 24/10/2020 v2.6.0
|
|
||||||
Added session save and restore for Konsole
|
|
||||||
|
|
||||||
# 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
|
|
||||||
|
|
||||||
# 06/03/2020 v2.5.2
|
|
||||||
Few aliases sorted out
|
|
||||||
|
|
||||||
# 05/03/2020 v2.5.1
|
|
||||||
Language consistancy fix
|
|
||||||
Added pigz support in taz
|
|
||||||
|
|
||||||
# 03/03/2020 v2.5.0
|
|
||||||
Added command taz and rmspc
|
|
||||||
Renamed auzip => utaz and improved it
|
|
||||||
|
|
||||||
# 02/03/2020 v2.4.0
|
|
||||||
Added command auzip
|
|
||||||
|
|
||||||
# 31/01/2020 v2.3.2
|
|
||||||
Figlet: changed default font to ansi_shadow
|
|
||||||
|
|
||||||
# 16/01/2020 v2.3.1
|
|
||||||
[bugfix] non-interactive were blocked with some functions
|
|
||||||
|
|
||||||
# 08/01/2020 v2.3.0
|
|
||||||
Added use of figlet and neofetch as a motd replace
|
|
||||||
|
|
||||||
# 16/12/2019 v2.2.0
|
|
||||||
Added showinfo
|
|
||||||
Primary write of showdiskmap
|
|
||||||
|
|
||||||
# 24/09/2019 v2.1.2
|
|
||||||
[bugfix] bug in profile version display
|
|
||||||
|
|
||||||
# 23/09/2019 v2.1.1
|
|
||||||
[bugfix] dpkgs
|
|
||||||
|
|
||||||
# 16/09/2018 v2.1.0
|
|
||||||
Added rmhost, setc, setfr
|
|
||||||
More locales management
|
|
||||||
|
|
||||||
# 04/02/2017 v2.0.1
|
|
||||||
clean improvements (--shell)
|
|
||||||
|
|
||||||
# 24/10/2015 v2.0.0
|
|
||||||
Added advanced functionnalities (clean, srr, etc.)
|
|
||||||
|
|
||||||
# 16/02/2013 v1.0.0
|
|
||||||
Initial version
|
|
||||||
|
|
||||||
@@ -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
|
||||||
+484
-105
@@ -34,6 +34,14 @@
|
|||||||
# * OF SUCH DAMAGE.
|
# * OF SUCH DAMAGE.
|
||||||
# ------------------------------------------------------------------------------
|
# ------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
# shellcheck disable=SC2317,SC2329 # nested helper functions are called through their name (shellcheck cannot follow them)
|
||||||
|
|
||||||
|
# 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 "compress"
|
||||||
|
|
||||||
# ------------------------------------------------------------------------------
|
# ------------------------------------------------------------------------------
|
||||||
# Smartly uncompress archives
|
# Smartly uncompress archives
|
||||||
# Usage: utaz [option] [directorie(s)|file(s)]
|
# Usage: utaz [option] [directorie(s)|file(s)]
|
||||||
@@ -42,99 +50,147 @@
|
|||||||
# -d, --delete If decompression succeeded, delete the source file
|
# -d, --delete If decompression succeeded, delete the source file
|
||||||
# -c, --create-dir Always create a host directory
|
# -c, --create-dir Always create a host directory
|
||||||
# -n, --no-dir Never create a host directory
|
# -n, --no-dir Never create a host directory
|
||||||
|
# -i, --ignore-case Match archive extensions case-insensitively (.ZIP, .Tar.GZ)
|
||||||
|
# -s, --case-sensitive Match archive extensions case-sensitively (overrides the
|
||||||
|
# UTAZ_DEFAULT_IGNORE_CASE configuration key)
|
||||||
utaz()
|
utaz()
|
||||||
{
|
{
|
||||||
|
# Extractor helpers. Every one takes the archive path as $1 and an existing
|
||||||
|
# target directory as $2, and returns the extractor exit status (0 = ok,
|
||||||
|
# 1 = warning, anything else = failure). The matching one is selected by
|
||||||
|
# file extension and invoked by name (see the dispatch "case" below), and
|
||||||
|
# the name minus its "_un" prefix is the binary checked beforehand.
|
||||||
|
# NOTE: bash defines functions globally, so these helpers are (re)defined in
|
||||||
|
# the shell on each call and the "local" lines below only declare an unused
|
||||||
|
# variable of the same name; they are kept as a marker for shellcheck.
|
||||||
|
local _ununzip
|
||||||
# shellcheck disable=SC2329
|
# shellcheck disable=SC2329
|
||||||
_ununzip()
|
_ununzip()
|
||||||
{
|
{
|
||||||
unzip -o "$1" -d "$2" >/dev/null 2>&1
|
unzip -o "$1" -d "$2" >/dev/null 2>&1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
local _untar
|
||||||
# shellcheck disable=SC2329
|
# shellcheck disable=SC2329
|
||||||
_untar()
|
_untar()
|
||||||
{
|
{
|
||||||
tar -xf "$1" -C "$2"
|
tar -xf "$1" -C "$2"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
local _ungzip
|
||||||
# shellcheck disable=SC2329
|
# shellcheck disable=SC2329
|
||||||
_ungzip()
|
_ungzip()
|
||||||
{
|
{
|
||||||
tar -xzf "$1" -C "$2"
|
tar -xzf "$1" -C "$2"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
local _unbzip2
|
||||||
# shellcheck disable=SC2329
|
# shellcheck disable=SC2329
|
||||||
_unbzip2()
|
_unbzip2()
|
||||||
{
|
{
|
||||||
tar -xjf "$1" -C "$2"
|
tar -xjf "$1" -C "$2"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
local _unxz
|
||||||
# shellcheck disable=SC2329
|
# shellcheck disable=SC2329
|
||||||
_unxz()
|
_unxz()
|
||||||
{
|
{
|
||||||
tar -xJf "$1" -C "$2"
|
tar -xJf "$1" -C "$2"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
local _unlzop
|
||||||
# shellcheck disable=SC2329
|
# shellcheck disable=SC2329
|
||||||
_unlzop()
|
_unlzop()
|
||||||
{
|
{
|
||||||
lzop -d "$1" -o "$2/$(basename "${1%.*}")"
|
lzop -d "$1" -o "$2/$(basename "${1%.*}")"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
local _unlzip
|
||||||
# shellcheck disable=SC2329
|
# shellcheck disable=SC2329
|
||||||
_unlzip()
|
_unlzip()
|
||||||
{
|
{
|
||||||
if command -v plzip >/dev/null 2>&1; then
|
# Only *.tar.lz / *.tlz reach this helper: decompress to stdout and
|
||||||
plzip -d -c "$1" > "$2/$(basename "${1%.*}")"
|
# untar from the pipe. pipefail (subshell-scoped) makes a lzip failure
|
||||||
else
|
# visible instead of being masked by tar's exit status.
|
||||||
lzip -d -c "$1" > "$2/$(basename "${1%.*}")"
|
local lzcmd=lzip
|
||||||
fi
|
command -v plzip >/dev/null 2>&1 && lzcmd=plzip
|
||||||
|
(
|
||||||
|
set -o pipefail
|
||||||
|
"$lzcmd" -d -c -- "$1" | tar -xf - -C "$2"
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
local _ununrar
|
||||||
# shellcheck disable=SC2329
|
# shellcheck disable=SC2329
|
||||||
_ununrar()
|
_ununrar()
|
||||||
{
|
{
|
||||||
unrar x -o+ "$1" "$2/" >/dev/null 2>&1
|
unrar x -o+ "$1" "$2/" >/dev/null 2>&1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
local _ununarj
|
||||||
# shellcheck disable=SC2329
|
# shellcheck disable=SC2329
|
||||||
_ununarj()
|
_ununarj()
|
||||||
{
|
{
|
||||||
unarj e "$1" "$2/" >/dev/null 2>&1
|
unarj e "$1" "$2/" >/dev/null 2>&1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
local _unlha
|
||||||
# shellcheck disable=SC2329
|
# shellcheck disable=SC2329
|
||||||
_unlha()
|
_unlha()
|
||||||
{
|
{
|
||||||
# lha typically extracts into the current directory
|
# lha extracts into the current directory, so we cd into the target
|
||||||
# We ensure it hits the target directory
|
# directory and feed it an absolute path to the archive.
|
||||||
(cd "$2" && lha -x "../$1") >/dev/null 2>&1
|
local src
|
||||||
|
src=$(realpath -- "$1") || return 1
|
||||||
|
(cd "$2" && lha -x "$src") >/dev/null 2>&1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
local _ununace
|
||||||
# shellcheck disable=SC2329
|
# shellcheck disable=SC2329
|
||||||
_ununace()
|
_ununace()
|
||||||
{
|
{
|
||||||
unace x "$1" "$2/" >/dev/null 2>&1
|
unace x "$1" "$2/" >/dev/null 2>&1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
local _un7z
|
||||||
# shellcheck disable=SC2329
|
# shellcheck disable=SC2329
|
||||||
_un7z()
|
_un7z()
|
||||||
{
|
{
|
||||||
7z x "$1" -o"$2/" >/dev/null 2>&1
|
7z x "$1" -o"$2/" >/dev/null 2>&1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
local _unzstd
|
||||||
# shellcheck disable=SC2329
|
# shellcheck disable=SC2329
|
||||||
_unzstd()
|
_unzstd()
|
||||||
{
|
{
|
||||||
# Zstd decompresses files directly, often requiring tar for archives
|
# A tar.zst/.tzst is untarred; a bare .zst is a single compressed file
|
||||||
tar --zstd -xf "$1" -C "$2"
|
# and is simply decompressed into the target directory.
|
||||||
|
# Called from utaz: "icase" is utaz's local, visible here through
|
||||||
|
# dynamic scoping. Lowercase the name so the match follows the mode.
|
||||||
|
local key=$1
|
||||||
|
[[ -n $icase ]] && key=${key,,}
|
||||||
|
case $key in
|
||||||
|
*.tar.zst|*.tzst)
|
||||||
|
tar --zstd -xf "$1" -C "$2"
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
zstd -d -q -c -- "$1" > "$2/$(basename "${1%.*}")"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
}
|
}
|
||||||
|
|
||||||
|
local _uncpio
|
||||||
# shellcheck disable=SC2329
|
# shellcheck disable=SC2329
|
||||||
_uncpio()
|
_uncpio()
|
||||||
{
|
{
|
||||||
# CPIO requires careful directory handling
|
# cpio extracts into the current directory, so we cd into the target
|
||||||
(cd "$2" && cpio -id < "../$1") >/dev/null 2>&1
|
# directory and read from an absolute path to the archive.
|
||||||
|
local src
|
||||||
|
src=$(realpath -- "$1") || return 1
|
||||||
|
(cd "$2" && cpio -id < "$src") >/dev/null 2>&1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
local _uncabextract
|
||||||
# shellcheck disable=SC2329
|
# shellcheck disable=SC2329
|
||||||
_uncabextract()
|
_uncabextract()
|
||||||
{
|
{
|
||||||
@@ -142,6 +198,7 @@ utaz()
|
|||||||
cabextract "$1" -d "$2/" >/dev/null 2>&1
|
cabextract "$1" -d "$2/" >/dev/null 2>&1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
local _undeb
|
||||||
# shellcheck disable=SC2329
|
# shellcheck disable=SC2329
|
||||||
_undeb()
|
_undeb()
|
||||||
{
|
{
|
||||||
@@ -149,22 +206,63 @@ utaz()
|
|||||||
dpkg-deb -x "$1" "$2/" >/dev/null 2>&1
|
dpkg-deb -x "$1" "$2/" >/dev/null 2>&1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
local _unrpm
|
||||||
# shellcheck disable=SC2329
|
# shellcheck disable=SC2329
|
||||||
_unrpm()
|
_unrpm()
|
||||||
{
|
{
|
||||||
# Extracts CPIO-based payload from an RPM package
|
# Extracts CPIO-based payload from an RPM package
|
||||||
# Needs rpm2cpio and cpio
|
# Needs rpm2cpio and cpio. pipefail (subshell-scoped) so that a failing
|
||||||
rpm2cpio "$1" | (cd "$2/" && cpio -idmv) >/dev/null 2>&1
|
# rpm2cpio is not masked by cpio's exit status.
|
||||||
|
(
|
||||||
|
set -o pipefail
|
||||||
|
rpm2cpio "$1" | (cd "$2/" && cpio -idm)
|
||||||
|
) >/dev/null 2>&1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Move every entry (hidden ones included) of directory $1 up into its
|
||||||
|
# parent, then remove $1. Refuses (returns 1, $1 left untouched) if any
|
||||||
|
# entry would overwrite something already present in the parent.
|
||||||
|
# The directory is first renamed to a temporary name so that an archive
|
||||||
|
# holding a top-level entry named like the host directory (foo.zip -> foo/foo)
|
||||||
|
# does not collide with itself. Runs in a subshell so the shopt calls do not
|
||||||
|
# leak into the interactive shell.
|
||||||
|
local _utaz_flatten
|
||||||
|
# shellcheck disable=SC2329
|
||||||
|
_utaz_flatten()
|
||||||
|
(
|
||||||
|
shopt -s nullglob dotglob
|
||||||
|
local dir=$1 parent tmp child
|
||||||
|
parent=$(dirname -- "$dir")
|
||||||
|
tmp="${parent}/.utaz.$$.${RANDOM}"
|
||||||
|
mv -- "$dir" "$tmp" || return 1
|
||||||
|
for child in "$tmp"/*; do
|
||||||
|
if [[ -e "${parent}/${child##*/}" || -L "${parent}/${child##*/}" ]]; then
|
||||||
|
disp W "${parent}/${child##*/} already exists, not overwriting."
|
||||||
|
mv -- "$tmp" "$dir"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
for child in "$tmp"/*; do
|
||||||
|
mv -- "$child" "${parent}/" || return 1
|
||||||
|
done
|
||||||
|
rmdir -- "$tmp"
|
||||||
|
)
|
||||||
|
|
||||||
local PARSED
|
local PARSED
|
||||||
PARSED=$(getopt -o hdcn --long help,delete,create-dir,no-dir -n 'utaz' -- "$@")
|
PARSED=$(getopt -o hdcnis --long help,delete,create-dir,no-dir,ignore-case,case-sensitive -n 'utaz' -- "$@")
|
||||||
# shellcheck disable=SC2181 # getopt return code is checked immediately after
|
# shellcheck disable=SC2181 # getopt return code is checked immediately after
|
||||||
if [ $? -ne 0 ]; then
|
if [ $? -ne 0 ]; then
|
||||||
disp E "Invalid options, use \"utaz --help\" to display usage."
|
disp E "Invalid options, use \"utaz --help\" to display usage."
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
eval set -- "$PARSED"
|
# getopt emits a shell-quoted, normalised argument list ("--" terminates the
|
||||||
|
# options); _getopt_split rebuilds the arguments from it (spaces and quotes
|
||||||
|
# intact, no eval). The flags are declared local so that none leaks in from
|
||||||
|
# the caller's environment.
|
||||||
|
local willrm="" createdir="" nodir="" icase="" csens=""
|
||||||
|
local -a _pargs=()
|
||||||
|
_getopt_split _pargs "$PARSED"
|
||||||
|
set -- "${_pargs[@]}"
|
||||||
while true; do
|
while true; do
|
||||||
case "$1" in
|
case "$1" in
|
||||||
-h|--help)
|
-h|--help)
|
||||||
@@ -175,19 +273,23 @@ utaz()
|
|||||||
printf "\t-h, --help\t\tDisplay that help screen\n"
|
printf "\t-h, --help\t\tDisplay that help screen\n"
|
||||||
printf "\t-d, --delete\t\tIf decompression succeeded, delete the source file\n"
|
printf "\t-d, --delete\t\tIf decompression succeeded, delete the source file\n"
|
||||||
printf "\t-c, --create-dir\tAlways create a host directory\n"
|
printf "\t-c, --create-dir\tAlways create a host directory\n"
|
||||||
printf "\t-n, --no-dir\t\tNever create a host directory\n\n"
|
printf "\t-n, --no-dir\t\tNever create a host directory\n"
|
||||||
|
printf "\t-i, --ignore-case\tMatch archive extensions case-insensitively\n"
|
||||||
|
printf "\t-s, --case-sensitive\tMatch archive extensions case-sensitively (default)\n\n"
|
||||||
printf "Supported archive format:\n"
|
printf "Supported archive format:\n"
|
||||||
printf "\t- zip\n"
|
printf "\t- zip\n"
|
||||||
printf "\t- tar.gz, .tgz\n"
|
printf "\t- tar.gz, .tgz\n"
|
||||||
printf "\t- tar.bz2, .tbz2\n"
|
printf "\t- tar.bz2, .tbz2\n"
|
||||||
printf "\t- tar.xz, .txz\n"
|
printf "\t- tar.xz, .txz\n"
|
||||||
printf "\t- tar.lz, .tlz\n"
|
printf "\t- tar.lz, .tlz\n"
|
||||||
|
printf "\t- lzo\n"
|
||||||
printf "\t- rar\n"
|
printf "\t- rar\n"
|
||||||
printf "\t- arj\n"
|
printf "\t- arj\n"
|
||||||
printf "\t- lha, lzh\n"
|
printf "\t- lha, lzh\n"
|
||||||
printf "\t- ace\n"
|
printf "\t- ace\n"
|
||||||
printf "\t- 7z, p7z\n"
|
printf "\t- 7z, p7z\n"
|
||||||
printf "\t- zst\n"
|
printf "\t- tar\n"
|
||||||
|
printf "\t- zst, tar.zst, .tzst\n"
|
||||||
printf "\t- cpio\n"
|
printf "\t- cpio\n"
|
||||||
printf "\t- cab\n"
|
printf "\t- cab\n"
|
||||||
printf "\t- deb\n"
|
printf "\t- deb\n"
|
||||||
@@ -195,15 +297,23 @@ utaz()
|
|||||||
return 0
|
return 0
|
||||||
;;
|
;;
|
||||||
-d|--delete)
|
-d|--delete)
|
||||||
local willrm=1
|
willrm=1
|
||||||
shift
|
shift
|
||||||
;;
|
;;
|
||||||
-c|--create-dir)
|
-c|--create-dir)
|
||||||
local createdir=1
|
createdir=1
|
||||||
shift
|
shift
|
||||||
;;
|
;;
|
||||||
-n|--no-dir)
|
-n|--no-dir)
|
||||||
local nodir=1
|
nodir=1
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
-i|--ignore-case)
|
||||||
|
icase=1
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
-s|--case-sensitive)
|
||||||
|
csens=1
|
||||||
shift
|
shift
|
||||||
;;
|
;;
|
||||||
--)
|
--)
|
||||||
@@ -222,24 +332,48 @@ utaz()
|
|||||||
[[ ${#FILES[@]} -eq 0 ]] && FILES=(".")
|
[[ ${#FILES[@]} -eq 0 ]] && FILES=(".")
|
||||||
|
|
||||||
# Apply defaults from [compress] configuration if not overridden by flags
|
# Apply defaults from [compress] configuration if not overridden by flags
|
||||||
[[ -z ${willrm+x} ]] && [[ ${UTAZ_DEFAULT_DELETE:-0} -eq 1 ]] && willrm=1
|
[[ -z $willrm ]] && [[ ${UTAZ_DEFAULT_DELETE:-0} -eq 1 ]] && willrm=1
|
||||||
if [[ -z ${createdir+x} && -z ${nodir+x} ]]; then
|
if [[ -z $createdir && -z $nodir ]]; then
|
||||||
case "${UTAZ_DEFAULT_DIR_MODE:-auto}" in
|
case "${UTAZ_DEFAULT_DIR_MODE:-auto}" in
|
||||||
always) createdir=1 ;;
|
always) createdir=1 ;;
|
||||||
never) nodir=1 ;;
|
never) nodir=1 ;;
|
||||||
esac
|
esac
|
||||||
fi
|
fi
|
||||||
|
|
||||||
[[ -n ${createdir} && -n ${nodir} ]] && \
|
[[ -n ${createdir} && -n ${nodir} ]] && {
|
||||||
disp E "The --create-dir and --no-dir options are mutually exclusive."
|
disp E "The --create-dir and --no-dir options are mutually exclusive."
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Extension matching: the flags win over the configured default. After this
|
||||||
|
# block "icase" is non-empty if and only if matching is case-insensitive.
|
||||||
|
[[ -n $icase && -n $csens ]] && {
|
||||||
|
disp E "The --ignore-case and --case-sensitive options are mutually exclusive."
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
if [[ -z $icase && -z $csens && ${UTAZ_DEFAULT_IGNORE_CASE:-0} -eq 1 ]]; then
|
||||||
|
icase=1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Loop variables are local: this function lives in the interactive shell.
|
||||||
|
local zitem f
|
||||||
for zitem in "${FILES[@]}"; do
|
for zitem in "${FILES[@]}"; do
|
||||||
|
# A name starting with "-" would be taken as an option by the tools below
|
||||||
|
[[ $zitem == -* ]] && zitem="./${zitem}"
|
||||||
|
|
||||||
# Build list of input files to process, with whitespace-safe handling.
|
# Build list of input files to process, with whitespace-safe handling.
|
||||||
local targets=()
|
local targets=()
|
||||||
if [[ -f "$zitem" ]]; then
|
if [[ -f "$zitem" ]]; then
|
||||||
targets=("$zitem")
|
targets=("$zitem")
|
||||||
elif [[ -d "$zitem" ]]; then
|
elif [[ -d "$zitem" ]]; then
|
||||||
mapfile -d '' -t targets < <(find "$zitem" -mindepth 1 -maxdepth 1 -print0 2>/dev/null)
|
# Only direct children are considered (no recursion); NUL-separated
|
||||||
|
# so names containing spaces or newlines survive.
|
||||||
|
# ("mapfile -d" needs bash 4.4, the profile supports 4.3)
|
||||||
|
local _zt
|
||||||
|
targets=()
|
||||||
|
while IFS= read -r -d '' _zt; do
|
||||||
|
targets+=("$_zt")
|
||||||
|
done < <(find "$zitem" -mindepth 1 -maxdepth 1 -print0 2>/dev/null)
|
||||||
if [[ ${#targets[@]} -eq 0 ]]; then
|
if [[ ${#targets[@]} -eq 0 ]]; then
|
||||||
disp I "Directory ${zitem} is empty, skipping."
|
disp I "Directory ${zitem} is empty, skipping."
|
||||||
continue
|
continue
|
||||||
@@ -250,9 +384,19 @@ utaz()
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
for f in "${targets[@]}"; do
|
for f in "${targets[@]}"; do
|
||||||
|
# "key" is the name the extension patterns are matched against:
|
||||||
|
# lowercased in ignore-case mode, untouched otherwise.
|
||||||
|
local key=$f
|
||||||
|
[[ -n $icase ]] && key=${f,,}
|
||||||
|
|
||||||
|
# Host directory name: archive name without its extension. A
|
||||||
|
# trailing ".tar" is dropped too, so foo.tar.gz gives "foo", not
|
||||||
|
# "foo.tar". The ".tar" test uses "key" so it follows the case mode
|
||||||
|
# (the extension is 4 characters either way).
|
||||||
local dir="${f%.*}"
|
local dir="${f%.*}"
|
||||||
|
[[ ${key%.*} == *.tar ]] && dir="${dir%.*}"
|
||||||
local extractor=""
|
local extractor=""
|
||||||
case "$f" in
|
case "$key" in
|
||||||
*.zip)
|
*.zip)
|
||||||
extractor="_ununzip"
|
extractor="_ununzip"
|
||||||
;;
|
;;
|
||||||
@@ -266,6 +410,9 @@ utaz()
|
|||||||
extractor="_unxz"
|
extractor="_unxz"
|
||||||
;;
|
;;
|
||||||
*.tar.lz|*.tlz)
|
*.tar.lz|*.tlz)
|
||||||
|
extractor="_unlzip"
|
||||||
|
;;
|
||||||
|
*.lzo)
|
||||||
extractor="_unlzop"
|
extractor="_unlzop"
|
||||||
;;
|
;;
|
||||||
*.tar)
|
*.tar)
|
||||||
@@ -286,7 +433,7 @@ utaz()
|
|||||||
*.7z|*.p7z)
|
*.7z|*.p7z)
|
||||||
extractor="_un7z"
|
extractor="_un7z"
|
||||||
;;
|
;;
|
||||||
*.zst)
|
*.zst|*.tzst)
|
||||||
extractor="_unzstd"
|
extractor="_unzstd"
|
||||||
;;
|
;;
|
||||||
*.cpio)
|
*.cpio)
|
||||||
@@ -307,7 +454,9 @@ utaz()
|
|||||||
;; # Skip non-archive files
|
;; # Skip non-archive files
|
||||||
esac
|
esac
|
||||||
|
|
||||||
# Verify binary existence
|
# Verify the extractor's binary exists. The helper name minus its
|
||||||
|
# "_un" prefix is the program name (_un7z -> 7z); deb/rpm/lzip need
|
||||||
|
# special handling because their programs are named differently.
|
||||||
local cmd=${extractor//_un/}
|
local cmd=${extractor//_un/}
|
||||||
if [[ $cmd == "deb" ]]; then
|
if [[ $cmd == "deb" ]]; then
|
||||||
command -v -- dpkg-deb >/dev/null 2>&1 || {
|
command -v -- dpkg-deb >/dev/null 2>&1 || {
|
||||||
@@ -323,6 +472,12 @@ utaz()
|
|||||||
disp E "The program 'cpio' is not installed, aborting."
|
disp E "The program 'cpio' is not installed, aborting."
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
elif [[ $cmd == "lzip" ]]; then
|
||||||
|
# plzip is a drop-in parallel replacement for lzip
|
||||||
|
command -v -- lzip >/dev/null 2>&1 || command -v -- plzip >/dev/null 2>&1 || {
|
||||||
|
disp E "Neither 'lzip' nor 'plzip' is installed, skipping ${f}."
|
||||||
|
continue
|
||||||
|
}
|
||||||
else
|
else
|
||||||
command -v -- "${cmd}" >/dev/null 2>&1 || {
|
command -v -- "${cmd}" >/dev/null 2>&1 || {
|
||||||
disp E "Binary ${cmd} necessary to extract ${f} is missing."
|
disp E "Binary ${cmd} necessary to extract ${f} is missing."
|
||||||
@@ -331,18 +486,23 @@ utaz()
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
disp I "Processing archive ${f} with ${extractor}..."
|
disp I "Processing archive ${f} with ${extractor}..."
|
||||||
if ! mkdir -p "${dir}"; then
|
# Track whether we create the target directory so a corrupted
|
||||||
disp E "The filesystem can't create directories, exit!" &&
|
# archive never wipes a pre-existing directory of the same name.
|
||||||
return 1
|
local dir_created=0
|
||||||
|
[[ -d "${dir}" ]] || dir_created=1
|
||||||
|
if ! mkdir -p -- "${dir}"; then
|
||||||
|
disp E "Can't create directory ${dir}, skipping ${f}."
|
||||||
|
continue
|
||||||
fi
|
fi
|
||||||
|
|
||||||
${extractor} "${f}" "${dir}"
|
${extractor} "${f}" "${dir}"
|
||||||
case $? in
|
case $? in
|
||||||
0)
|
0)
|
||||||
[[ -n ${willrm} ]] &&
|
[[ -n ${willrm} ]] &&
|
||||||
rm -f "${f}" && disp I "File ${zitem}/${f} deleted."
|
rm -f -- "${f}" && disp I "File ${f} deleted."
|
||||||
;;
|
;;
|
||||||
1)
|
1)
|
||||||
|
# Most extractors use 1 for "finished with warnings"
|
||||||
if [[ -n ${willrm} ]]; then
|
if [[ -n ${willrm} ]]; then
|
||||||
disp W "Compression program returned a warning: deletion canceled."
|
disp W "Compression program returned a warning: deletion canceled."
|
||||||
else
|
else
|
||||||
@@ -351,40 +511,40 @@ utaz()
|
|||||||
;;
|
;;
|
||||||
*)
|
*)
|
||||||
disp E "The compressed file ${f} seems corrupted, failed."
|
disp E "The compressed file ${f} seems corrupted, failed."
|
||||||
rm -rf "${dir}" >/dev/null 2>&1
|
[[ ${dir_created} -eq 1 ]] && rm -rf -- "${dir}" >/dev/null 2>&1
|
||||||
continue
|
continue
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
|
# Decide whether the host directory is kept. Flattening is never
|
||||||
|
# done on a directory that existed before extraction, as it would
|
||||||
|
# also move the user's own files out of it.
|
||||||
|
local flatten=0
|
||||||
if [[ -n ${createdir} ]]; then
|
if [[ -n ${createdir} ]]; then
|
||||||
disp I "Archive extracted successfully in subdirectory."
|
:
|
||||||
|
elif [[ ${dir_created} -ne 1 ]]; then
|
||||||
|
:
|
||||||
elif [[ -n ${nodir} ]]; then
|
elif [[ -n ${nodir} ]]; then
|
||||||
shopt -s nullglob
|
flatten=1
|
||||||
for child in "${dir}"/*; do
|
else
|
||||||
mv -- "$child" .
|
# Auto mode: flatten only if the archive already holds a single
|
||||||
done
|
# top-level directory (it then acts as the host directory).
|
||||||
shopt -u nullglob
|
# nullglob makes the array empty when nothing matches; dotglob
|
||||||
rmdir -- "${dir}"
|
# makes hidden entries count, so ".git" alone is not ignored.
|
||||||
|
local _had_ng=0 _had_dg=0
|
||||||
|
shopt -q nullglob && _had_ng=1
|
||||||
|
shopt -q dotglob && _had_dg=1
|
||||||
|
shopt -s nullglob dotglob
|
||||||
|
local contents=( "${dir}"/* )
|
||||||
|
(( _had_ng )) || shopt -u nullglob
|
||||||
|
(( _had_dg )) || shopt -u dotglob
|
||||||
|
[[ ${#contents[@]} -eq 1 && -d "${contents[0]}" && ! -L "${contents[0]}" ]] && flatten=1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ $flatten -eq 1 ]] && _utaz_flatten "${dir}"; then
|
||||||
disp I "Archive extracted successfully, no subdirectory needed."
|
disp I "Archive extracted successfully, no subdirectory needed."
|
||||||
else
|
else
|
||||||
# Set nullglob to ensure the array is empty if no files match
|
disp I "Archive extracted successfully in subdirectory."
|
||||||
shopt -s nullglob
|
|
||||||
local contents=( "${dir}"/* )
|
|
||||||
|
|
||||||
# Check if exactly one item exists and if that item is a directory
|
|
||||||
if [[ ${#contents[@]} -eq 1 ]] && [[ -d "${contents[0]}" ]]; then
|
|
||||||
# Single directory detected
|
|
||||||
shopt -s nullglob
|
|
||||||
for child in "${contents[0]}"/*; do
|
|
||||||
mv -- "$child" .
|
|
||||||
done
|
|
||||||
shopt -u nullglob
|
|
||||||
rmdir -- "${dir}"
|
|
||||||
disp I "Archive extracted successfully, no subdirectory needed."
|
|
||||||
else
|
|
||||||
disp I "Archive extracted successfully in subdirectory."
|
|
||||||
fi
|
|
||||||
shopt -u nullglob
|
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
done
|
done
|
||||||
@@ -395,18 +555,54 @@ export -f utaz
|
|||||||
|
|
||||||
# ------------------------------------------------------------------------------
|
# ------------------------------------------------------------------------------
|
||||||
# Compress directories or files into one or more archive
|
# Compress directories or files into one or more archive
|
||||||
# Usage: taz [option] [--parallel=<n>] [--format=<format>] [directory1 ... directoryN]
|
# Usage: taz [option] [--parallel=<n|auto>] [--format=<format>] [directory1 ... directoryN]
|
||||||
# Options:
|
# Options:
|
||||||
# -h, --help Display that help screen
|
# -h, --help Display that help screen
|
||||||
# -d, --delete Delete source file or directory after success
|
# -d, --delete Delete source file or directory after success
|
||||||
# -f, --format Chose archive format in the given list. If several format are
|
# -f, --format Chose archive format in the given list. If several format are
|
||||||
# given, the smalest is kept
|
# given, the smalest is kept
|
||||||
# -p, --parallel Number of threads to use (if allowed by underlying utility)
|
# -K, --keep-all Keep every produced version instead of only the smallest
|
||||||
|
# -p, --parallel Number of threads to use, or 'auto' to use detected CPU count
|
||||||
# -v, --verbose Display progress where possible
|
# -v, --verbose Display progress where possible
|
||||||
# -q, --quiet Display less messages (only errors and warnings)
|
# -q, --quiet Display less messages (only errors and warnings)
|
||||||
# -1, .., -9 Compression level to use [1=fast/biggest, 9=slow/smallest]
|
# -1, .., -9 Compression level to use [1=fast/biggest, 9=slow/smallest]
|
||||||
taz()
|
taz()
|
||||||
{
|
{
|
||||||
|
# Compressor helpers. All share the same signature:
|
||||||
|
# $1 = file to compress, $2 = thread count, $3 = level (1-9),
|
||||||
|
# $4 = non-empty for verbose output
|
||||||
|
# and keep the source (--keep), writing "$1.<ext>" next to it. They return
|
||||||
|
# 127 when no suitable program is installed (fatal for the whole run), any
|
||||||
|
# other non-zero status on a compression failure (that format is skipped).
|
||||||
|
# As with utaz, nested functions are global, "local" does not scope them.
|
||||||
|
|
||||||
|
# Resolve runtime CPU count for --parallel=auto.
|
||||||
|
local _taz_detect_cpus
|
||||||
|
_taz_detect_cpus()
|
||||||
|
{
|
||||||
|
local cpus=1
|
||||||
|
if command -v nproc >/dev/null 2>&1; then
|
||||||
|
cpus=$(nproc 2>/dev/null)
|
||||||
|
elif command -v getconf >/dev/null 2>&1; then
|
||||||
|
cpus=$(getconf _NPROCESSORS_ONLN 2>/dev/null)
|
||||||
|
fi
|
||||||
|
|
||||||
|
[[ $cpus =~ ^[1-9][0-9]*$ ]] || cpus=1
|
||||||
|
printf "%s\n" "$cpus"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Render a byte count as a human-readable size (B/KB/MB/GB/...), base 1024.
|
||||||
|
local _taz_human
|
||||||
|
# shellcheck disable=SC2329
|
||||||
|
_taz_human()
|
||||||
|
{
|
||||||
|
if command -v numfmt >/dev/null 2>&1; then
|
||||||
|
numfmt --to=iec --suffix=B -- "$1" 2>/dev/null && return 0
|
||||||
|
fi
|
||||||
|
printf "%sB\n" "$1"
|
||||||
|
}
|
||||||
|
|
||||||
|
local _doxz
|
||||||
# shellcheck disable=SC2329
|
# shellcheck disable=SC2329
|
||||||
_doxz()
|
_doxz()
|
||||||
{
|
{
|
||||||
@@ -427,10 +623,11 @@ taz()
|
|||||||
return $?
|
return $?
|
||||||
}
|
}
|
||||||
|
|
||||||
|
local _dolz
|
||||||
# shellcheck disable=SC2329
|
# shellcheck disable=SC2329
|
||||||
_dolz()
|
_dolz()
|
||||||
{
|
{
|
||||||
local procopt="--threads $2"
|
local procopt=(--threads "$2")
|
||||||
local command=plzip
|
local command=plzip
|
||||||
|
|
||||||
command -v plzip >/dev/null 2>&1 || {
|
command -v plzip >/dev/null 2>&1 || {
|
||||||
@@ -439,7 +636,7 @@ taz()
|
|||||||
return 127
|
return 127
|
||||||
}
|
}
|
||||||
command=lzip
|
command=lzip
|
||||||
local procopt=""
|
local procopt=()
|
||||||
[[ $2 -gt 1 ]] &&
|
[[ $2 -gt 1 ]] &&
|
||||||
disp W "lzip doesn't support multithreading, falling back to 1 thread." &&
|
disp W "lzip doesn't support multithreading, falling back to 1 thread." &&
|
||||||
disp W "Consider installing plzip to obtain multithreading abilities."
|
disp W "Consider installing plzip to obtain multithreading abilities."
|
||||||
@@ -447,17 +644,18 @@ taz()
|
|||||||
|
|
||||||
local opt=()
|
local opt=()
|
||||||
[[ $4 ]] && opt=('-vv')
|
[[ $4 ]] && opt=('-vv')
|
||||||
opt+=("$procopt")
|
opt+=("${procopt[@]}")
|
||||||
|
|
||||||
# Compress with lzip (lzma)
|
# Compress with lzip (lzma)
|
||||||
$command "${opt[@]}" --keep "-$3" "$1"
|
$command "${opt[@]}" --keep "-$3" "$1"
|
||||||
return $?
|
return $?
|
||||||
}
|
}
|
||||||
|
|
||||||
|
local _dogz
|
||||||
# shellcheck disable=SC2329
|
# shellcheck disable=SC2329
|
||||||
_dogz()
|
_dogz()
|
||||||
{
|
{
|
||||||
local procopt="--processes $2"
|
local procopt=(--processes "$2")
|
||||||
local command=pigz
|
local command=pigz
|
||||||
|
|
||||||
command -v pigz >/dev/null 2>&1 || {
|
command -v pigz >/dev/null 2>&1 || {
|
||||||
@@ -465,8 +663,8 @@ taz()
|
|||||||
disp E "Programs 'pigz' or 'gzip' are not installed, aborting."
|
disp E "Programs 'pigz' or 'gzip' are not installed, aborting."
|
||||||
return 127
|
return 127
|
||||||
}
|
}
|
||||||
local command="gzip --compress"
|
local command=gzip
|
||||||
local procopt=""
|
local procopt=()
|
||||||
[[ $2 -gt 1 ]] &&
|
[[ $2 -gt 1 ]] &&
|
||||||
disp W "gzip doesn't support multithreading, falling back to 1 thread." &&
|
disp W "gzip doesn't support multithreading, falling back to 1 thread." &&
|
||||||
disp W "Consider installing pigz to obtain multithreading abilities."
|
disp W "Consider installing pigz to obtain multithreading abilities."
|
||||||
@@ -474,17 +672,18 @@ taz()
|
|||||||
|
|
||||||
local opt=()
|
local opt=()
|
||||||
[[ $4 ]] && opt=('--verbose')
|
[[ $4 ]] && opt=('--verbose')
|
||||||
opt+=("$procopt")
|
opt+=("${procopt[@]}")
|
||||||
|
|
||||||
# Compresse au format bz2
|
# Compress with gzip
|
||||||
$command "${opt[@]}" --keep "-$3" "$1"
|
$command "${opt[@]}" --keep "-$3" "$1"
|
||||||
return $?
|
return $?
|
||||||
}
|
}
|
||||||
|
|
||||||
|
local _dobz2
|
||||||
# shellcheck disable=SC2329
|
# shellcheck disable=SC2329
|
||||||
_dobz2()
|
_dobz2()
|
||||||
{
|
{
|
||||||
local procopt="-p$2"
|
local procopt=("-p$2")
|
||||||
local command=pbzip2
|
local command=pbzip2
|
||||||
|
|
||||||
command -v pbzip2 >/dev/null 2>&1 || {
|
command -v pbzip2 >/dev/null 2>&1 || {
|
||||||
@@ -493,7 +692,7 @@ taz()
|
|||||||
return 127
|
return 127
|
||||||
}
|
}
|
||||||
local command=bzip2
|
local command=bzip2
|
||||||
local procopt=""
|
local procopt=()
|
||||||
[[ $2 -gt 1 ]] &&
|
[[ $2 -gt 1 ]] &&
|
||||||
disp W "bzip2 doesn't support multithreading, falling back to 1 thread." &&
|
disp W "bzip2 doesn't support multithreading, falling back to 1 thread." &&
|
||||||
disp W "Consider installing pbzip2 to obtain multithreading abilities."
|
disp W "Consider installing pbzip2 to obtain multithreading abilities."
|
||||||
@@ -501,13 +700,14 @@ taz()
|
|||||||
|
|
||||||
local opt=()
|
local opt=()
|
||||||
[[ $4 ]] && opt=('-v')
|
[[ $4 ]] && opt=('-v')
|
||||||
opt+=("$procopt")
|
opt+=("${procopt[@]}")
|
||||||
|
|
||||||
# Compress with bz2
|
# Compress with bz2
|
||||||
$command "${opt[@]}" --compress --keep "-$3" "$1"
|
$command "${opt[@]}" --compress --keep "-$3" "$1"
|
||||||
return $?
|
return $?
|
||||||
}
|
}
|
||||||
|
|
||||||
|
local _dolzo
|
||||||
# shellcheck disable=SC2329
|
# shellcheck disable=SC2329
|
||||||
_dolzo()
|
_dolzo()
|
||||||
{
|
{
|
||||||
@@ -520,30 +720,38 @@ taz()
|
|||||||
[[ $4 ]] && verb=('-v')
|
[[ $4 ]] && verb=('-v')
|
||||||
[[ $2 -gt 1 ]] && disp W "lzop doesn't support multithreading, falling back to 1 thread."
|
[[ $2 -gt 1 ]] && disp W "lzop doesn't support multithreading, falling back to 1 thread."
|
||||||
|
|
||||||
# Compresse au format lzo
|
# Compress with lzo
|
||||||
lzop "${verb[@]}" --keep "-$3" "$1"
|
lzop "${verb[@]}" --keep "-$3" "$1"
|
||||||
return $?
|
return $?
|
||||||
}
|
}
|
||||||
|
|
||||||
local PARSED
|
local PARSED
|
||||||
PARSED=$(getopt -o hdf:p:vq123456789 --long help,delete,format:,parallel:,verbose,quiet --name "taz" -- "$@")
|
PARSED=$(getopt -o hdf:Kp:vq123456789 --long help,delete,format:,keep-all,parallel:,verbose,quiet --name "taz" -- "$@")
|
||||||
# shellcheck disable=SC2181 # getopt return code is checked immediately after
|
# shellcheck disable=SC2181 # getopt return code is checked immediately after
|
||||||
if [ $? -ne 0 ]; then
|
if [ $? -ne 0 ]; then
|
||||||
disp E "Invalid options, use \"taz --help\" to display usage."
|
disp E "Invalid options, use \"taz --help\" to display usage."
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
eval set -- "$PARSED"
|
# See utaz: restore the getopt-normalised argument list, and keep every
|
||||||
|
# flag local so nothing leaks in from the caller's environment.
|
||||||
|
local willrm="" keepall="" verbose="" quiet="" nproc="" complevel=""
|
||||||
|
local -a _pargs=()
|
||||||
|
_getopt_split _pargs "$PARSED"
|
||||||
|
set -- "${_pargs[@]}"
|
||||||
|
local compforms=()
|
||||||
|
local _f _list
|
||||||
while true; do
|
while true; do
|
||||||
case "$1" in
|
case "$1" in
|
||||||
-h|--help)
|
-h|--help)
|
||||||
printf "taz: archive all files of a directory.\n\n"
|
printf "taz: archive all files of a directory.\n\n"
|
||||||
printf "Usage: taz [option] [--parallel=<n>] [--format=<format>] [directory1 ... directoryN]\n\n"
|
printf "Usage: taz [option] [--parallel=<n|auto>] [--format=<format>] [directory1 ... directoryN]\n\n"
|
||||||
printf "Options:\n"
|
printf "Options:\n"
|
||||||
printf "\t-h, --help\tDisplay that help screen\n"
|
printf "\t-h, --help\tDisplay that help screen\n"
|
||||||
printf "\t-d, --delete\tDelete source file or directory after success\n"
|
printf "\t-d, --delete\tDelete source file or directory after success\n"
|
||||||
printf "\t-f, --format\tChose archive format in the given list. If several format are"
|
printf "\t-f, --format\tChose archive format in the given list. If several format are\n"
|
||||||
printf "\t\t\tgiven, the smalest is kept\n"
|
printf "\t\t\tgiven, the smalest produced archive is kept\n"
|
||||||
printf "\t-p, --parallel\tNumber of threads to use (if allowed by underlying utility)\n"
|
printf "\t-K, --keep-all\tKeep every produced version instead of only the smallest\n"
|
||||||
|
printf "\t-p, --parallel\tNumber of threads, or 'auto' for runtime CPU count\n"
|
||||||
printf "\t-v, --verbose\tDisplay progress where possible\n"
|
printf "\t-v, --verbose\tDisplay progress where possible\n"
|
||||||
printf "\t-q, --quiet\tDisplay less messages (only errors and warnings)\n"
|
printf "\t-q, --quiet\tDisplay less messages (only errors and warnings)\n"
|
||||||
printf "\t-1, .., -9\tCompression level to use [1=fast/biggest, 9=slow/smallest]\n\n"
|
printf "\t-1, .., -9\tCompression level to use [1=fast/biggest, 9=slow/smallest]\n\n"
|
||||||
@@ -561,27 +769,36 @@ taz()
|
|||||||
;;
|
;;
|
||||||
|
|
||||||
-d|--delete)
|
-d|--delete)
|
||||||
local willrm=1
|
willrm=1
|
||||||
shift
|
shift
|
||||||
;;
|
;;
|
||||||
|
|
||||||
-f|--format)
|
-f|--format)
|
||||||
local compform=$2
|
# Accept comma/space separated lists and repeated -f flags.
|
||||||
|
# "read -a" splits without pathname expansion (unlike an unquoted
|
||||||
|
# expansion, which would glob "*" against the current directory).
|
||||||
|
IFS=', ' read -ra _list <<< "$2"
|
||||||
|
compforms+=("${_list[@]}")
|
||||||
shift 2
|
shift 2
|
||||||
;;
|
;;
|
||||||
|
|
||||||
|
-K|--keep-all)
|
||||||
|
keepall=1
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
|
||||||
-p|--parallel)
|
-p|--parallel)
|
||||||
local nproc=$2
|
nproc=$2
|
||||||
shift 2
|
shift 2
|
||||||
;;
|
;;
|
||||||
|
|
||||||
-v|--verbose)
|
-v|--verbose)
|
||||||
local verbose=1
|
verbose=1
|
||||||
shift
|
shift
|
||||||
;;
|
;;
|
||||||
|
|
||||||
-q|--quiet)
|
-q|--quiet)
|
||||||
local quiet=1
|
quiet=1
|
||||||
shift
|
shift
|
||||||
;;
|
;;
|
||||||
|
|
||||||
@@ -603,62 +820,224 @@ taz()
|
|||||||
# The remaining arguments after -- are the files/directories to process,
|
# The remaining arguments after -- are the files/directories to process,
|
||||||
# "." is used if none is given
|
# "." is used if none is given
|
||||||
local FILES=("$@")
|
local FILES=("$@")
|
||||||
[[ ${#FILES[@]} -eq 0 ]] && FILES=(".")
|
[[ ${#FILES[@]} -eq 0 ]] && FILES=(".")
|
||||||
|
|
||||||
[[ ! $compform ]] && compform=${TAZ_DEFAULT_FORMAT:-lz}
|
# Defaults from the [compress] configuration for anything not set by flags
|
||||||
[[ ! $nproc ]] && nproc=${TAZ_DEFAULT_THREADS:-1}
|
[[ -z $nproc ]] && nproc=${TAZ_DEFAULT_THREADS:-auto}
|
||||||
[[ ! $complevel ]] && complevel=${TAZ_DEFAULT_LEVEL:-6}
|
[[ -z $complevel ]] && complevel=${TAZ_DEFAULT_LEVEL:-6}
|
||||||
[[ $verbose -gt 1 && $quiet -gt 1 ]] &&
|
if [[ ! $complevel =~ ^[1-9]$ ]]; then
|
||||||
|
disp E "Invalid compression level '${complevel}' (expected 1 to 9)."
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
[[ -n $verbose && -n $quiet ]] && {
|
||||||
disp E "The --verbose and --quiet options can't be used together."
|
disp E "The --verbose and --quiet options can't be used together."
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
# disp() hides info messages when QUIET=1; being local, this only lasts
|
||||||
|
# for the duration of this call (dynamic scoping).
|
||||||
|
# shellcheck disable=SC2034 # read by disp
|
||||||
|
[[ -n $quiet ]] && local QUIET=1
|
||||||
|
|
||||||
|
# Fall back to the configured (or built-in) default format list.
|
||||||
|
if [[ ${#compforms[@]} -eq 0 ]]; then
|
||||||
|
IFS=', ' read -ra _list <<< "${TAZ_DEFAULT_FORMAT:-lz}"
|
||||||
|
compforms+=("${_list[@]}")
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Reject unknown formats early so we never dispatch to a missing _do*.
|
||||||
|
# Membership test: the padded list is matched against " <fmt> " so that
|
||||||
|
# only whole words match (e.g. "z" or "lz xz" are rejected).
|
||||||
|
local _supported=" lz xz bz2 gz lzo tar "
|
||||||
|
for _f in "${compforms[@]}"; do
|
||||||
|
[[ $_supported == *" $_f "* ]] || {
|
||||||
|
disp E "Unsupported format '${_f}' (supported: lz xz bz2 gz lzo tar)."
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
done
|
||||||
|
|
||||||
|
# Backward compatibility: 0 previously meant auto-detect.
|
||||||
|
[[ $nproc == 0 ]] && nproc=auto
|
||||||
|
if [[ $nproc == auto ]]; then
|
||||||
|
nproc=$(_taz_detect_cpus)
|
||||||
|
elif [[ ! $nproc =~ ^[1-9][0-9]*$ ]]; then
|
||||||
|
disp E "Invalid value for --parallel: '$nproc' (expected auto or a positive integer)."
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Map each format to the extension its compressor appends to the input
|
||||||
|
# name. "tar" is handled separately (its output is always "$item.tar").
|
||||||
|
local -A _taz_ext=( [lz]=lz [xz]=xz [bz2]=bz2 [gz]=gz [lzo]=lzo )
|
||||||
|
|
||||||
|
local item
|
||||||
for item in "${FILES[@]}"; do
|
for item in "${FILES[@]}"; do
|
||||||
local donetar=0
|
local donetar=0
|
||||||
|
# Arguments naming the tar members; the default keeps the path as typed
|
||||||
|
local tar_src=()
|
||||||
|
|
||||||
|
# Normalise the name, since archive names are built as "$item.<ext>":
|
||||||
|
# - strip trailing slashes (shell completion adds them), otherwise
|
||||||
|
# "dir/.tar" would be created inside the directory being archived;
|
||||||
|
# - "." / ".." (or "x/..") have no usable name, so resolve them;
|
||||||
|
# - protect names starting with "-" from being read as options.
|
||||||
|
while [[ ${#item} -gt 1 && $item == */ ]]; do
|
||||||
|
item=${item%/}
|
||||||
|
done
|
||||||
|
case ${item##*/} in
|
||||||
|
.|..)
|
||||||
|
item=$(realpath -- "$item") || continue
|
||||||
|
# Store the bare directory name, not its absolute path
|
||||||
|
tar_src=(-C "${item%/*}" -- "${item##*/}")
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
if [[ $item == / ]]; then
|
||||||
|
disp E "Refusing to archive the root directory, skipping."
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
[[ $item == -* ]] && item="./${item}"
|
||||||
|
[[ ${#tar_src[@]} -eq 0 ]] && tar_src=(-- "$item")
|
||||||
|
|
||||||
disp I "Processing $item..."
|
disp I "Processing $item..."
|
||||||
|
if [[ ! -e $item ]]; then
|
||||||
|
disp W "Path ${item} does not exist, skipping."
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
if [[ -d "$item" ]]; then
|
if [[ -d "$item" ]]; then
|
||||||
|
# Directories can only be compressed through an intermediate tar.
|
||||||
|
if [[ -e "$item.tar" ]]; then
|
||||||
|
disp E "${item}.tar already exists, not overwriting it, skipping."
|
||||||
|
continue
|
||||||
|
fi
|
||||||
disp I "\t Creating $item.tar... "
|
disp I "\t Creating $item.tar... "
|
||||||
|
|
||||||
if ! tar -cf "$item.tar" "$item"; then
|
if ! tar -cf "$item.tar" "${tar_src[@]}"; then
|
||||||
disp E "tar file creation failed, skipping to next item."
|
disp E "tar file creation failed, skipping to next item."
|
||||||
|
rm -f -- "$item.tar"
|
||||||
continue
|
continue
|
||||||
fi
|
fi
|
||||||
|
|
||||||
local donetar=1
|
donetar=1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# File actually handed to the compressors
|
||||||
local fname=$item
|
local fname=$item
|
||||||
[[ $donetar -gt 0 ]] && fname=$item.tar
|
[[ $donetar -gt 0 ]] && fname=$item.tar
|
||||||
|
|
||||||
# Skip compression part if tar is asked
|
# Compress the archive with every requested format; the smallest
|
||||||
if [[ $compform != "tar" ]]; then
|
# resulting file is kept and the others are discarded (unless --keep-all).
|
||||||
disp I "\t Compressing archive..."
|
local candidates=() fmt exec_code aborted=0 tar_requested=0
|
||||||
local exec_code=0
|
for fmt in "${compforms[@]}"; do
|
||||||
"_do$compform" "$fname" "$nproc" "$complevel" "$verbose" || exec_code=$?
|
if [[ $fmt == tar ]]; then
|
||||||
[[ ! $exec_code -eq 0 ]] && case $exec_code in
|
tar_requested=1
|
||||||
|
if [[ $donetar -eq 0 ]]; then
|
||||||
|
# Plain file: "tar" means wrapping it in a tar. It must be
|
||||||
|
# a separate candidate, never the source file itself, as
|
||||||
|
# non-winning candidates get deleted below.
|
||||||
|
if [[ -e "$item.tar" ]]; then
|
||||||
|
disp W "${item}.tar already exists, skipping the tar format."
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
if ! tar -cf "$item.tar" "${tar_src[@]}"; then
|
||||||
|
disp W "tar creation failed, skipping this format."
|
||||||
|
rm -f -- "$item.tar"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
candidates+=("$item.tar")
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
disp I "\t Compressing archive with ${fmt}..."
|
||||||
|
# "_do<fmt>" is resolved dynamically (formats were validated above).
|
||||||
|
# "|| exec_code=$?" captures the status without tripping set -e.
|
||||||
|
exec_code=0
|
||||||
|
"_do$fmt" "$fname" "$nproc" "$complevel" "$verbose" || exec_code=$?
|
||||||
|
case $exec_code in
|
||||||
|
0)
|
||||||
|
candidates+=("$fname.${_taz_ext[$fmt]}")
|
||||||
|
;;
|
||||||
127)
|
127)
|
||||||
disp E "Compression program unavailable, aborting."
|
disp E "Compression program for '${fmt}' unavailable, aborting."
|
||||||
return 127
|
aborted=1
|
||||||
|
break
|
||||||
;;
|
;;
|
||||||
*)
|
*)
|
||||||
disp E "Compression program returned an error, not deleting anything if asked, skipping to next item."
|
disp W "Compression with '${fmt}' failed, skipping this format."
|
||||||
continue
|
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
[[ $donetar -gt 0 ]] && rm "$fname"
|
# Remove partial output produced before an unrecoverable abort.
|
||||||
|
local c
|
||||||
|
if [[ $aborted -eq 1 ]]; then
|
||||||
|
for c in "${candidates[@]}"; do
|
||||||
|
[[ $c != "$fname" ]] && rm -f -- "$c"
|
||||||
|
done
|
||||||
|
[[ $donetar -gt 0 ]] && rm -f -- "$fname"
|
||||||
|
return 127
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Select the smallest archive among the produced candidates.
|
||||||
|
# _csize maps each existing candidate to its size in bytes; winner_size
|
||||||
|
# starts at -1 meaning "no candidate seen yet".
|
||||||
|
local winner="" winner_size=-1 sz
|
||||||
|
local -A _csize=()
|
||||||
|
for c in "${candidates[@]}"; do
|
||||||
|
[[ -f $c ]] || continue
|
||||||
|
sz=$(stat -c%s -- "$c" 2>/dev/null) || continue
|
||||||
|
_csize["$c"]=$sz
|
||||||
|
if [[ $winner_size -lt 0 || $sz -lt $winner_size ]]; then
|
||||||
|
winner_size=$sz
|
||||||
|
winner=$c
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [[ -z $winner ]]; then
|
||||||
|
disp E "No archive could be produced for ${item}, skipping to next item."
|
||||||
|
[[ $donetar -gt 0 ]] && rm -f -- "$fname"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
# In verbose mode, list every produced archive with its size and how
|
||||||
|
# much larger it is than the smallest one that will be kept.
|
||||||
|
if [[ -n $verbose && ${#_csize[@]} -gt 0 ]]; then
|
||||||
|
disp I "\t Produced archives:"
|
||||||
|
for c in "${candidates[@]}"; do
|
||||||
|
[[ -n ${_csize[$c]+x} ]] || continue
|
||||||
|
if [[ $c == "$winner" ]]; then
|
||||||
|
disp "\t ${c} $(_taz_human "${_csize[$c]}") (kept, smallest)"
|
||||||
|
else
|
||||||
|
disp "\t ${c} $(_taz_human "${_csize[$c]}") (+$(_taz_human "$(( ${_csize[$c]} - winner_size ))"))"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Discard every archive but the smallest, unless --keep-all was given.
|
||||||
|
# Either way, drop the intermediate tar when it was not itself a
|
||||||
|
# requested output format.
|
||||||
|
if [[ -z $keepall ]]; then
|
||||||
|
for c in "${candidates[@]}"; do
|
||||||
|
[[ $c != "$winner" ]] && rm -f -- "$c"
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
[[ $donetar -gt 0 && $tar_requested -eq 0 ]] && rm -f -- "$fname"
|
||||||
|
|
||||||
|
if [[ -n $keepall ]]; then
|
||||||
|
[[ ${#compforms[@]} -gt 1 ]] &&
|
||||||
|
disp I "\t Kept all produced archives (smallest: ${winner}, $(_taz_human "$winner_size"))."
|
||||||
|
else
|
||||||
|
[[ ${#compforms[@]} -gt 1 ]] &&
|
||||||
|
disp I "\t Kept smallest archive: ${winner} ($(_taz_human "$winner_size"))."
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ $willrm ]]; then
|
if [[ $willrm ]]; then
|
||||||
disp I "\t Deleting original source as asked... "
|
disp I "\t Deleting original source as asked... "
|
||||||
rm -r "$item"
|
rm -r -- "$item"
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
unset quiet
|
|
||||||
}
|
}
|
||||||
export -f taz
|
export -f taz
|
||||||
# ------------------------------------------------------------------------------
|
# ------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
load_conf "compress"
|
|
||||||
|
|
||||||
# EOF
|
# EOF
|
||||||
|
|||||||
@@ -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
|
||||||
+125
-36
@@ -34,84 +34,106 @@
|
|||||||
# * OF SUCH DAMAGE.
|
# * 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 "debug"
|
||||||
|
|
||||||
# ------------------------------------------------------------------------------
|
# ------------------------------------------------------------------------------
|
||||||
# Display a backtrace
|
# 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
|
# Usage: backtrace
|
||||||
function 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"
|
printf "========= Call stack =========\n"
|
||||||
local i=1 # We begin at 1 to ignore backtrace itself
|
local i=1 # We begin at 1 to ignore backtrace itself
|
||||||
while [[ $i -lt ${#FUNCNAME[@]} ]]; do
|
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' \
|
printf '%15s() %s:%d\n' \
|
||||||
"${FUNCNAME[$i]}" "${BASH_SOURCE[$i]}" "${BASH_LINENO[$(( i-1 ))]}"
|
"${FUNCNAME[$i]}" "${BASH_SOURCE[$i]}" "${BASH_LINENO[$(( i-1 ))]}"
|
||||||
((i++))
|
i=$(( i + 1 ))
|
||||||
done
|
done
|
||||||
unset i
|
|
||||||
printf "==============================\n"
|
printf "==============================\n"
|
||||||
}
|
}
|
||||||
|
export -f backtrace
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
# ------------------------------------------------------------------------------
|
# ------------------------------------------------------------------------------
|
||||||
# Function to be trapped for errors investigation
|
# Handler installed by settrace as ERR trap: report the failure and the call
|
||||||
function error()
|
# 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=$?
|
local errcode=$?
|
||||||
backtrace
|
printf "Command failed with exit status %d\n" "$errcode" >&2
|
||||||
|
backtrace >&2
|
||||||
return $errcode
|
return $errcode
|
||||||
}
|
}
|
||||||
|
export -f error
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
|
||||||
# ------------------------------------------------------------------------------
|
# ------------------------------------------------------------------------------
|
||||||
# Activate or deactivate error trapping to display backtrace
|
# Activate or deactivate error trapping to display backtrace
|
||||||
# Usage: settrace <--on|--off|--status>
|
# Usage: settrace <--on|--off|--status> [--force]
|
||||||
|
# Startup behaviour is set by SETTRACE_DEFAULT_ENABLED in the [debug] section.
|
||||||
settrace()
|
settrace()
|
||||||
{
|
{
|
||||||
local status="off"
|
|
||||||
[[ $(trap -p ERR) ]] && status="on"
|
|
||||||
#trap -p ERR
|
|
||||||
|
|
||||||
local PARSED
|
local PARSED
|
||||||
PARSED=$(getopt -oh --long help,on,off,status,force -- "$@")
|
PARSED=$(getopt -oh --long help,on,off,status,force -n settrace -- "$@")
|
||||||
# shellcheck disable=SC2181 # getopt return code is checked immediately after
|
# shellcheck disable=SC2181 # getopt return code is checked immediately after
|
||||||
if [[ $? -ne 0 ]]; then
|
if [[ $? -ne 0 ]]; then
|
||||||
disp E "Invalid options, use \"settrace --help\" to display usage."
|
disp E "Invalid options, use \"settrace --help\" to display usage."
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
eval set -- "$PARSED"
|
# getopt emits a shell-quoted, normalised argument list ("--" ends options)
|
||||||
local force=0
|
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
|
while true; do
|
||||||
case $1 in
|
case $1 in
|
||||||
-h|--help)
|
-h|--help)
|
||||||
printf "Try to activate backtrace display for script debugging.\n\n"
|
printf "Try to activate backtrace display for script debugging.\n\n"
|
||||||
printf "Options:\n"
|
printf "Options:\n"
|
||||||
printf "\t--on\t\tActivate backtrace generation\n"
|
printf "\t--on\t\tActivate backtrace generation\n"
|
||||||
printf "\t--force\t\tForce replacement of existing trap (use with --on)\n"
|
printf "\t--force\t\tReplace (with --on) or remove (with --off) an ERR trap\n"
|
||||||
printf "\t--off\t\tDeactivate backtrace generation\n\n"
|
printf "\t\t\tnot installed by settrace\n"
|
||||||
printf "That function active a trap event on error. If the script you want to\n"
|
printf "\t--off\t\tDeactivate backtrace generation\n"
|
||||||
printf "debug overload the ERR bash trap, it will not work.\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
|
return 0
|
||||||
;;
|
;;
|
||||||
--on)
|
--on|--off|--status)
|
||||||
if [[ ${status} == "on" ]] && [[ $force -eq 0 ]]; then
|
if [[ -n $action ]]; then
|
||||||
disp E "ERR signal trap is already set. Use --force to replace it."
|
disp E "The --on, --off and --status options are mutually exclusive."
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
trap "error" ERR
|
action="${1#--}"
|
||||||
shift
|
shift
|
||||||
;;
|
;;
|
||||||
--force)
|
--force)
|
||||||
force=1
|
force=1
|
||||||
shift
|
shift
|
||||||
;;
|
;;
|
||||||
--off)
|
|
||||||
if [[ ${status} != "on" ]]; then
|
|
||||||
disp W "ERR signal trap is already unset!"
|
|
||||||
fi
|
|
||||||
trap - ERR
|
|
||||||
shift
|
|
||||||
;;
|
|
||||||
--status)
|
|
||||||
disp I "Trap signal is ${status}."
|
|
||||||
shift
|
|
||||||
;;
|
|
||||||
--)
|
--)
|
||||||
shift
|
shift
|
||||||
break
|
break
|
||||||
@@ -119,13 +141,80 @@ settrace()
|
|||||||
*)
|
*)
|
||||||
disp E "Invalid options, use \"settrace --help\" to display usage."
|
disp E "Invalid options, use \"settrace --help\" to display usage."
|
||||||
return 1
|
return 1
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
done
|
done
|
||||||
unset status force
|
|
||||||
|
# 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
|
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
|
# EOF
|
||||||
|
|||||||
+677
-16
@@ -34,6 +34,12 @@
|
|||||||
# * OF SUCH DAMAGE.
|
# * 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
|
# Color definitions
|
||||||
set_colors()
|
set_colors()
|
||||||
@@ -42,7 +48,7 @@ set_colors()
|
|||||||
export DEFAULTFG='\e[0;39m'
|
export DEFAULTFG='\e[0;39m'
|
||||||
export DEFAULTBG='\e[0;49m'
|
export DEFAULTBG='\e[0;49m'
|
||||||
export DEFAULTCOL="${DEFAULTBG}${DEFAULTFG}"
|
export DEFAULTCOL="${DEFAULTBG}${DEFAULTFG}"
|
||||||
export RESETCOL=$'\e[0m'
|
export RESETCOL='\e[0m'
|
||||||
|
|
||||||
# Regular Colors
|
# Regular Colors
|
||||||
export Black='\e[0;30m'
|
export Black='\e[0;30m'
|
||||||
@@ -122,16 +128,128 @@ export -f set_colors
|
|||||||
# Display a message
|
# Display a message
|
||||||
# Usage: disp <type> <message>
|
# Usage: disp <type> <message>
|
||||||
# Types:
|
# Types:
|
||||||
# I : info (green)
|
# I : info (green), stdout
|
||||||
# W : warning (yellow)
|
# W : warning (yellow), stderr
|
||||||
# E : error (red)
|
# E : error (red), stderr
|
||||||
# D : debug (cyan)
|
# 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()
|
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
|
# Handle NO_COLOR: disable colors if set
|
||||||
local color_enabled=1
|
local color_enabled=1
|
||||||
[[ -n $NO_COLOR ]] && color_enabled=0
|
[[ -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
|
case ${1^^} in
|
||||||
"I")
|
"I")
|
||||||
if [[ $color_enabled -eq 1 ]]; then
|
if [[ $color_enabled -eq 1 ]]; then
|
||||||
@@ -140,8 +258,9 @@ disp()
|
|||||||
local heads="[ info ]"
|
local heads="[ info ]"
|
||||||
fi
|
fi
|
||||||
shift
|
shift
|
||||||
[[ -z $QUIET || $QUIET -ne 1 ]] && \
|
if [[ ${QUIET:-} != 1 ]]; then
|
||||||
printf "%b\n" "${heads} $*${RESETCOL}"
|
_disp_print_wrapped "$heads" 1 "$*"
|
||||||
|
fi
|
||||||
;;
|
;;
|
||||||
"W")
|
"W")
|
||||||
if [[ $color_enabled -eq 1 ]]; then
|
if [[ $color_enabled -eq 1 ]]; then
|
||||||
@@ -150,7 +269,7 @@ disp()
|
|||||||
local heads="[ Warning ]"
|
local heads="[ Warning ]"
|
||||||
fi
|
fi
|
||||||
shift
|
shift
|
||||||
printf "%b\n" "${heads} $*${RESETCOL}" >&2
|
_disp_print_wrapped "$heads" 2 "$*"
|
||||||
;;
|
;;
|
||||||
"E")
|
"E")
|
||||||
if [[ $color_enabled -eq 1 ]]; then
|
if [[ $color_enabled -eq 1 ]]; then
|
||||||
@@ -159,7 +278,7 @@ disp()
|
|||||||
local heads="[ ERROR ]"
|
local heads="[ ERROR ]"
|
||||||
fi
|
fi
|
||||||
shift
|
shift
|
||||||
printf "%b\n" "${heads} $*${RESETCOL}" >&2
|
_disp_print_wrapped "$heads" 2 "$*"
|
||||||
;;
|
;;
|
||||||
"D")
|
"D")
|
||||||
if [[ $color_enabled -eq 1 ]]; then
|
if [[ $color_enabled -eq 1 ]]; then
|
||||||
@@ -168,12 +287,17 @@ disp()
|
|||||||
local heads="[ debug ]"
|
local heads="[ debug ]"
|
||||||
fi
|
fi
|
||||||
shift
|
shift
|
||||||
[[ -n $DEBUG && $DEBUG -gt 1 ]] && \
|
# Shown only when DEBUG is an integer greater than 1; sent to stderr
|
||||||
printf "%b\n" "${heads} $*${RESETCOL}"
|
# like the other diagnostics, so it never pollutes captured output.
|
||||||
|
if [[ ${DEBUG:-} =~ ^[0-9]+$ && $DEBUG -gt 1 ]]; then
|
||||||
|
_disp_print_wrapped "$heads" 2 "$*"
|
||||||
|
fi
|
||||||
;;
|
;;
|
||||||
* )
|
* )
|
||||||
[[ -z $QUIET || $QUIET -ne 1 ]] && \
|
# No (known) type: the whole argument list is the message, as for info
|
||||||
printf "%b\n" "$*"
|
if [[ ${QUIET:-} != 1 ]]; then
|
||||||
|
_disp_print_wrapped "" 1 "$*"
|
||||||
|
fi
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
}
|
}
|
||||||
@@ -181,8 +305,545 @@ export -f disp
|
|||||||
# ------------------------------------------------------------------------------
|
# ------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
# Load disp section variables
|
# ------------------------------------------------------------------------------
|
||||||
load_conf 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
|
set_colors
|
||||||
|
|
||||||
# EOF
|
# EOF
|
||||||
|
|||||||
+297
-63
@@ -34,8 +34,16 @@
|
|||||||
# * OF SUCH DAMAGE.
|
# * 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 "filefct"
|
||||||
|
|
||||||
# ------------------------------------------------------------------------------
|
# ------------------------------------------------------------------------------
|
||||||
# Expand wildcards in a file/directory list and quote the results
|
# Expand wildcards in a file/directory list and quote the results: each match is
|
||||||
|
# wrapped in double quotes (with \, ", $ and ` escaped) and the matches are
|
||||||
|
# joined with the separator. Items without wildcards are quoted as they are.
|
||||||
# Usage: expandlist [options] <item1 [item2 ... itemN]>
|
# Usage: expandlist [options] <item1 [item2 ... itemN]>
|
||||||
expandlist()
|
expandlist()
|
||||||
{
|
{
|
||||||
@@ -47,7 +55,10 @@ expandlist()
|
|||||||
disp E "Invalid options, use \"expandlist --help\" to display usage."
|
disp E "Invalid options, use \"expandlist --help\" to display usage."
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
eval set -- "$PARSED"
|
# Restore the getopt-normalised (shell-quoted) argument list
|
||||||
|
local -a _pargs=()
|
||||||
|
_getopt_split _pargs "$PARSED"
|
||||||
|
set -- "${_pargs[@]}"
|
||||||
|
|
||||||
while true; do
|
while true; do
|
||||||
case "$1" in
|
case "$1" in
|
||||||
@@ -56,7 +67,7 @@ expandlist()
|
|||||||
printf "Usage: expandlist [options] <item1 [item2 ... itemN]>\n\n"
|
printf "Usage: expandlist [options] <item1 [item2 ... itemN]>\n\n"
|
||||||
printf "Options:\n"
|
printf "Options:\n"
|
||||||
printf "\t-h, --help\t\tDisplay this help screen\n"
|
printf "\t-h, --help\t\tDisplay this help screen\n"
|
||||||
printf "\t-s, --separator SEP\tSet output separator (default: space)\n"
|
printf "\t-s, --separator SEP\tSet output separator (default: space); \\\\n, \\\\t... are interpreted\n"
|
||||||
printf "\t-n, --newline\t\tUse a newline as separator\n"
|
printf "\t-n, --newline\t\tUse a newline as separator\n"
|
||||||
return 0
|
return 0
|
||||||
;;
|
;;
|
||||||
@@ -79,15 +90,25 @@ expandlist()
|
|||||||
esac
|
esac
|
||||||
done
|
done
|
||||||
|
|
||||||
|
# Backslash escapes in the separator (\n, \t, ...) are interpreted, so the
|
||||||
|
# configuration file can express a newline as EXPANDLIST_DEFAULT_SEPARATOR=\n
|
||||||
|
printf -v separator '%b' "$separator"
|
||||||
|
|
||||||
local item="" result="" matched=0
|
local item="" result="" matched=0
|
||||||
|
# Restore the caller's nullglob setting on exit instead of forcing it off.
|
||||||
|
local _had_nullglob=0
|
||||||
|
shopt -q nullglob && _had_nullglob=1
|
||||||
shopt -s nullglob
|
shopt -s nullglob
|
||||||
|
# An empty IFS keeps the unquoted expansion below from splitting on spaces
|
||||||
|
# (it would cut "my dir/*.txt" in two) while globbing still happens.
|
||||||
|
local IFS=
|
||||||
|
|
||||||
for item in "$@"; do
|
for item in "$@"; do
|
||||||
local expanded=()
|
local expanded=()
|
||||||
|
|
||||||
# True glob expansion when wildcards are present.
|
# True glob expansion when wildcards are present.
|
||||||
if [[ "$item" == *'*'* || "$item" == *'?'* || "$item" == *'['* ]]; then
|
if [[ "$item" == *'*'* || "$item" == *'?'* || "$item" == *'['* ]]; then
|
||||||
# shellcheck disable=SC2206 # We actually want the word splitting
|
# shellcheck disable=SC2206 # Intentional glob expansion, no splitting (IFS is empty)
|
||||||
expanded=( $item )
|
expanded=( $item )
|
||||||
else
|
else
|
||||||
expanded=( "$item" )
|
expanded=( "$item" )
|
||||||
@@ -97,16 +118,24 @@ expandlist()
|
|||||||
continue
|
continue
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
local content
|
||||||
for content in "${expanded[@]}"; do
|
for content in "${expanded[@]}"; do
|
||||||
if (( matched )); then
|
if (( matched )); then
|
||||||
result+="$separator"
|
result+="$separator"
|
||||||
fi
|
fi
|
||||||
|
# Escape what stays special inside double quotes so that the output
|
||||||
|
# can safely be re-evaluated (eval, command substitution, ...).
|
||||||
|
content=${content//\\/\\\\}
|
||||||
|
content=${content//\"/\\\"}
|
||||||
|
content=${content//\$/\\\$}
|
||||||
|
content=${content//\`/\\\`}
|
||||||
result+="\"$content\""
|
result+="\"$content\""
|
||||||
matched=1
|
matched=1
|
||||||
done
|
done
|
||||||
|
unset content
|
||||||
done
|
done
|
||||||
|
|
||||||
shopt -u nullglob
|
(( _had_nullglob )) || shopt -u nullglob
|
||||||
printf '%s\n' "$result"
|
printf '%s\n' "$result"
|
||||||
}
|
}
|
||||||
export -f expandlist
|
export -f expandlist
|
||||||
@@ -134,7 +163,10 @@ clean()
|
|||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
eval set -- "$PARSED"
|
# Restore the getopt-normalised (shell-quoted) argument list
|
||||||
|
local -a _pargs=()
|
||||||
|
_getopt_split _pargs "$PARSED"
|
||||||
|
set -- "${_pargs[@]}"
|
||||||
|
|
||||||
while true; do
|
while true; do
|
||||||
case "$1" in
|
case "$1" in
|
||||||
@@ -180,19 +212,32 @@ clean()
|
|||||||
(( ! recursive )) && findopt=(-maxdepth 1)
|
(( ! recursive )) && findopt=(-maxdepth 1)
|
||||||
(( ! force )) && rmopt=(-i)
|
(( ! force )) && rmopt=(-i)
|
||||||
|
|
||||||
|
local dir f
|
||||||
for dir in "${dirlist[@]}"; do
|
for dir in "${dirlist[@]}"; do
|
||||||
find "$dir" "${findopt[@]}" -type f \( -name "*~" -o -name "#*#" -o -name "*.bak" -o -name ".~*#" \) -print0 |
|
# "./" keeps a name starting with "-" from being read as a find option.
|
||||||
|
# -H follows a symlink given as argument (a link to a directory).
|
||||||
|
[[ $dir == -* ]] && dir="./$dir"
|
||||||
|
find -H "$dir" "${findopt[@]}" -type f \( -name "*~" -o -name "#*#" -o -name "*.bak" -o -name ".~*#" \) -print0 |
|
||||||
while IFS= read -r -d '' f; do
|
while IFS= read -r -d '' f; do
|
||||||
if (( outshell )); then
|
if (( outshell )); then
|
||||||
|
# %q prints a re-usable (shell-quoted) form of the name
|
||||||
if (( ${#rmopt[@]} )); then
|
if (( ${#rmopt[@]} )); then
|
||||||
printf 'rm %s -- "%s"\n' "${rmopt[*]}" "$f"
|
printf 'rm %s -- %q\n' "${rmopt[*]}" "$f"
|
||||||
else
|
else
|
||||||
printf 'rm -- "%s"\n' "$f"
|
printf 'rm -- %q\n' "$f"
|
||||||
fi
|
fi
|
||||||
|
elif (( force )); then
|
||||||
|
# No confirmation: rm reads no stdin, so leave it attached
|
||||||
|
# to the find pipe (a /dev/tty redirect would fail when there
|
||||||
|
# is no controlling terminal, e.g. cron).
|
||||||
|
rm -- "$f" || disp E "Failed to remove \"$f\"."
|
||||||
else
|
else
|
||||||
rm "${rmopt[@]}" -- "$f"
|
# Interactive rm -i: read the answer from the terminal, not
|
||||||
|
# from the find pipe that feeds this loop's stdin.
|
||||||
|
rm -i -- "$f" </dev/tty || disp E "Failed to remove \"$f\"."
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
|
unset f
|
||||||
done
|
done
|
||||||
}
|
}
|
||||||
export -f clean
|
export -f clean
|
||||||
@@ -212,15 +257,15 @@ mcd()
|
|||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ ! $# -eq 1 ]]; then
|
if (( $# != 1 )); then
|
||||||
disp E "Missing parameter. Use \"mcd --help\" to display usage."
|
disp E "Missing parameter. Use \"mcd --help\" to display usage."
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
if ! mkdir -pv "$1"; then
|
if ! mkdir -pv -- "$1"; then
|
||||||
disp E "Failed to create directory \"$1\"."
|
disp E "Failed to create directory \"$1\"."
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
if ! cd "$1"; then
|
if ! cd -- "$1"; then
|
||||||
disp E "Failed to change to directory \"$1\"."
|
disp E "Failed to change to directory \"$1\"."
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
@@ -245,13 +290,16 @@ rmspc()
|
|||||||
local mvopt=()
|
local mvopt=()
|
||||||
local PARSED
|
local PARSED
|
||||||
|
|
||||||
PARSED=$(getopt -o hr:c::vs --long help,recursive,subst-char::,verbose,shell -n 'rmspc' -- "$@")
|
PARSED=$(getopt -o hrc::vs --long help,recursive,subst-char::,verbose,shell -n 'rmspc' -- "$@")
|
||||||
# shellcheck disable=SC2181 # getopt return code is checked immediately after
|
# shellcheck disable=SC2181 # getopt return code is checked immediately after
|
||||||
if [[ $? -ne 0 ]]; then
|
if [[ $? -ne 0 ]]; then
|
||||||
disp E "Invalid options, use \"rmspc --help\" to display usage."
|
disp E "Invalid options, use \"rmspc --help\" to display usage."
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
eval set -- "$PARSED"
|
# Restore the getopt-normalised (shell-quoted) argument list
|
||||||
|
local -a _pargs=()
|
||||||
|
_getopt_split _pargs "$PARSED"
|
||||||
|
set -- "${_pargs[@]}"
|
||||||
|
|
||||||
while true; do
|
while true; do
|
||||||
case "$1" in
|
case "$1" in
|
||||||
@@ -261,11 +309,12 @@ rmspc()
|
|||||||
printf "Options:\n"
|
printf "Options:\n"
|
||||||
printf "\t-h, --help\t\tDisplay that help screen\n"
|
printf "\t-h, --help\t\tDisplay that help screen\n"
|
||||||
printf "\t-r, --recursive\t\tTreat subdirectories of the given directory\n"
|
printf "\t-r, --recursive\t\tTreat subdirectories of the given directory\n"
|
||||||
printf "\t-c, --subst-char\tChange the replacement character (default is underscore)\n"
|
printf "\t-c, --subst-char[=C]\tChange the replacement character (default is underscore)\n"
|
||||||
printf "\t-v, --verbose\t\tDisplay more details (recursive mode only)\n"
|
printf "\t-v, --verbose\t\tDisplay more details (recursive mode only)\n"
|
||||||
printf "\t-s, --shell\t\tDo nothing and display commands that would be executed\n\n"
|
printf "\t-s, --shell\t\tDo nothing and display commands that would be executed\n\n"
|
||||||
printf "Note: if the --subst-char option is given without parameters, spaces will be\n"
|
printf "Note: the character must be attached (-c- or --subst-char=-). If the option\n"
|
||||||
printf " replaced with nothing (concatenation).\n"
|
printf " is given without a character, spaces are replaced with nothing\n"
|
||||||
|
printf " (concatenation). An existing file is never overwritten.\n"
|
||||||
return 0
|
return 0
|
||||||
;;
|
;;
|
||||||
-r|--recursive)
|
-r|--recursive)
|
||||||
@@ -296,46 +345,82 @@ rmspc()
|
|||||||
esac
|
esac
|
||||||
done
|
done
|
||||||
|
|
||||||
|
# getopt only attaches an optional argument written as -cX or --subst-char=X;
|
||||||
|
# "-c X" leaves X behind as a positional argument, which rmspc does not take
|
||||||
|
# (it works on the current directory) and would otherwise silently ignore.
|
||||||
|
if [[ $# -gt 0 ]]; then
|
||||||
|
disp E "Unexpected argument '$1'. The replacement character must be attached: -c$1 or --subst-char=$1"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
[[ "$substchar" == "none" ]] && substchar=""
|
[[ "$substchar" == "none" ]] && substchar=""
|
||||||
|
# A slash would turn the new name into a path
|
||||||
|
if [[ "$substchar" == */* ]]; then
|
||||||
|
disp E "The replacement character must not contain a slash."
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
(( verb )) && mvopt=(-v)
|
(( verb )) && mvopt=(-v)
|
||||||
|
|
||||||
|
# Restore the caller's nullglob setting on exit instead of forcing it off.
|
||||||
|
local _had_nullglob=0
|
||||||
|
shopt -q nullglob && _had_nullglob=1
|
||||||
shopt -s nullglob
|
shopt -s nullglob
|
||||||
|
local f
|
||||||
for f in *; do
|
for f in *; do
|
||||||
if (( recurs )) && [[ -d "$f" ]]; then
|
# Symlinks to directories are not followed (no escape from the tree,
|
||||||
|
# no loops).
|
||||||
|
if (( recurs )) && [[ -d "$f" && ! -L "$f" ]]; then
|
||||||
(
|
(
|
||||||
local lastdir=$f
|
local lastdir=$f
|
||||||
(( verb )) && disp I "Entering directory $(pwd)/$f ..."
|
if (( verb )); then
|
||||||
|
disp I "Entering directory $(pwd)/$f ..."
|
||||||
|
fi
|
||||||
pushd "$f" >/dev/null || return 1
|
pushd "$f" >/dev/null || return 1
|
||||||
|
|
||||||
if (( substchar_set )); then
|
# Build the recursive argument list by value: the ${var:+-flag}
|
||||||
rmspc ${recurs:+-r} -c "$substchar" ${verb:+-v} ${shell:+-s}
|
# idiom would expand on the *string* "0" (non-empty) and wrongly
|
||||||
else
|
# force -v/-s on every recursive call.
|
||||||
rmspc ${recurs:+-r} ${verb:+-v} ${shell:+-s}
|
local rargs=(-r)
|
||||||
fi
|
(( verb )) && rargs+=(-v)
|
||||||
|
(( shell )) && rargs+=(-s)
|
||||||
|
# Attached form, mandatory for an optional argument (see above)
|
||||||
|
(( substchar_set )) && rargs+=(--subst-char="$substchar")
|
||||||
|
rmspc "${rargs[@]}"
|
||||||
|
|
||||||
popd >/dev/null || return 1
|
popd >/dev/null || return 1
|
||||||
(( verb )) && disp I "Leaving directory $(pwd)/$lastdir"
|
# "if" rather than "(( verb )) && ...": the subshell status is
|
||||||
|
# that of its last command and must stay 0 when verb is off.
|
||||||
|
if (( verb )); then
|
||||||
|
disp I "Leaving directory $(pwd)/$lastdir"
|
||||||
|
fi
|
||||||
)
|
)
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ "$f" == *" "* ]]; then
|
if [[ "$f" == *" "* ]]; then
|
||||||
local newf="${f// /${substchar}}"
|
# Quoted replacement: an unquoted "&" would stand for the match
|
||||||
|
local newf="${f// /"$substchar"}"
|
||||||
[[ "$f" == "$newf" ]] && continue
|
[[ "$f" == "$newf" ]] && continue
|
||||||
|
# Never overwrite (mv would silently replace an existing file)
|
||||||
|
if [[ -e "$newf" || -L "$newf" ]]; then
|
||||||
|
disp E "Cannot rename \"$f\": \"$newf\" already exists."
|
||||||
|
continue
|
||||||
|
fi
|
||||||
if (( shell )); then
|
if (( shell )); then
|
||||||
if (( ${#mvopt[@]} )); then
|
if (( ${#mvopt[@]} )); then
|
||||||
printf 'mv %s -- "%s" "%s"\n' "${mvopt[*]}" "$f" "$newf"
|
printf 'mv %s -- %q %q\n' "${mvopt[*]}" "$f" "$newf"
|
||||||
else
|
else
|
||||||
printf 'mv -- "%s" "%s"\n' "$f" "$newf"
|
printf 'mv -- %q %q\n' "$f" "$newf"
|
||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
mv "${mvopt[@]}" -- "$f" "$newf" || {
|
mv -n "${mvopt[@]}" -- "$f" "$newf" || {
|
||||||
disp E "Failed renaming \"$f\" to \"$newf\"."
|
disp E "Failed renaming \"$f\" to \"$newf\"."
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
shopt -u nullglob
|
unset f
|
||||||
|
(( _had_nullglob )) || shopt -u nullglob
|
||||||
}
|
}
|
||||||
export -f rmspc
|
export -f rmspc
|
||||||
# ------------------------------------------------------------------------------
|
# ------------------------------------------------------------------------------
|
||||||
@@ -345,7 +430,7 @@ export -f rmspc
|
|||||||
# Display statistics about a file tree
|
# Display statistics about a file tree
|
||||||
# Usage: file_stats [options] [path]
|
# Usage: file_stats [options] [path]
|
||||||
# Options:
|
# Options:
|
||||||
# -H, --human Human readable sizes\n"
|
# -H, --human Human readable sizes
|
||||||
# -d, --details Display details (min/max/average/median)
|
# -d, --details Display details (min/max/average/median)
|
||||||
# -m, --average Display only average size
|
# -m, --average Display only average size
|
||||||
# -M, --median Display only median size
|
# -M, --median Display only median size
|
||||||
@@ -370,7 +455,10 @@ file_stats()
|
|||||||
disp E "Invalid options, use \"file_stats --help\" to display usage."
|
disp E "Invalid options, use \"file_stats --help\" to display usage."
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
eval set -- "$PARSED"
|
# Restore the getopt-normalised (shell-quoted) argument list
|
||||||
|
local -a _pargs=()
|
||||||
|
_getopt_split _pargs "$PARSED"
|
||||||
|
set -- "${_pargs[@]}"
|
||||||
|
|
||||||
while true; do
|
while true; do
|
||||||
case "$1" in
|
case "$1" in
|
||||||
@@ -450,41 +538,73 @@ file_stats()
|
|||||||
done
|
done
|
||||||
|
|
||||||
[[ -n "$1" ]] && path="$1"
|
[[ -n "$1" ]] && path="$1"
|
||||||
|
if [[ ! -e "$path" ]]; then
|
||||||
|
disp E "file_stats: no such file or directory: ${path}"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
# "./" keeps a name starting with "-" from being read as a find option
|
||||||
|
[[ $path == -* ]] && path="./$path"
|
||||||
|
|
||||||
# Prepare find filters
|
# Prepare find filters
|
||||||
local find_cmd=(find "$path" -type f)
|
local find_cmd=(find "$path" -type f)
|
||||||
|
|
||||||
# Single extension filter
|
# Single extension filter (case-insensitive). When combined with
|
||||||
|
# --ext-list, a file must satisfy both, as find ANDs the tests.
|
||||||
if [[ -n "$ext_filter" ]]; then
|
if [[ -n "$ext_filter" ]]; then
|
||||||
find_cmd+=(-iname "*.$ext_filter")
|
find_cmd+=(-iname "*.$ext_filter")
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Extension list filter
|
# Extension list filter: ( -iname *.a -o -iname *.b ... )
|
||||||
|
local exts
|
||||||
if [[ -n "$ext_list" ]]; then
|
if [[ -n "$ext_list" ]]; then
|
||||||
IFS=',' read -ra exts <<< "$ext_list"
|
IFS=',' read -ra exts <<< "$ext_list"
|
||||||
find_cmd+=('(')
|
find_cmd+=('(')
|
||||||
|
local i
|
||||||
for i in "${!exts[@]}"; do
|
for i in "${!exts[@]}"; do
|
||||||
[[ $i -ne 0 ]] && find_cmd+=(-o)
|
[[ $i -ne 0 ]] && find_cmd+=(-o)
|
||||||
find_cmd+=(-iname "*.${exts[$i]}")
|
find_cmd+=(-iname "*.${exts[$i]}")
|
||||||
done
|
done
|
||||||
|
unset i
|
||||||
find_cmd+=(')')
|
find_cmd+=(')')
|
||||||
fi
|
fi
|
||||||
|
unset exts
|
||||||
|
|
||||||
# Minimum/maximum size filters (evaluated in bytes)
|
# Minimum/maximum size filters (evaluated in bytes)
|
||||||
|
if [[ -n "$min_size" || -n "$max_size" ]]; then
|
||||||
|
if ! command -v numfmt >/dev/null 2>&1; then
|
||||||
|
disp E "file_stats: --min/--max require 'numfmt' (GNU coreutils). Please install it."
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
local bytes
|
||||||
if [[ -n "$min_size" ]]; then
|
if [[ -n "$min_size" ]]; then
|
||||||
find_cmd+=(-size +"$(numfmt --from=iec "$min_size")"c)
|
bytes=$(numfmt --from=iec "$min_size" 2>/dev/null) || {
|
||||||
|
disp E "file_stats: invalid --min size: ${min_size}"
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
# find's "-size +N" means "strictly more than N bytes"; the minimum
|
||||||
|
# is inclusive, hence N-1 (nothing to filter for a minimum of 0).
|
||||||
|
(( bytes > 0 )) && find_cmd+=(-size +"$(( bytes - 1 ))"c)
|
||||||
fi
|
fi
|
||||||
if [[ -n "$max_size" ]]; then
|
if [[ -n "$max_size" ]]; then
|
||||||
find_cmd+=(-size -"$(( $(numfmt --from=iec "$max_size") + 1 ))"c)
|
bytes=$(numfmt --from=iec "$max_size" 2>/dev/null) || {
|
||||||
|
disp E "file_stats: invalid --max size: ${max_size}"
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
# "-size -N" means "less than N bytes": N+1 makes the maximum inclusive.
|
||||||
|
find_cmd+=(-size -"$(( bytes + 1 ))"c)
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Execution
|
# Execution: one size per line, sorted ascending (the median needs it),
|
||||||
|
# then summarised by awk. The path goes through ENVIRON, as awk -v would
|
||||||
|
# interpret backslashes in it.
|
||||||
"${find_cmd[@]}" -printf "%s\n" 2>/dev/null | sort -n | \
|
"${find_cmd[@]}" -printf "%s\n" 2>/dev/null | sort -n | \
|
||||||
|
FILE_STATS_PATH="$path" \
|
||||||
awk -v human="$human" -v details="$details" -v only_avg="$only_avg" \
|
awk -v human="$human" -v details="$details" -v only_avg="$only_avg" \
|
||||||
-v only_med="$only_med" -v only_count="$only_count" \
|
-v only_med="$only_med" -v only_count="$only_count" \
|
||||||
-v only_total="$only_total" -v show_all="$show_all" -v path="$path" '
|
-v only_total="$only_total" -v show_all="$show_all" '
|
||||||
# Convert function
|
# Convert function (i and units are local: extra parameters)
|
||||||
function human_readable(x) {
|
function human_readable(x, i, units) {
|
||||||
split("B KiB MiB GiB TiB", units)
|
split("B KiB MiB GiB TiB", units)
|
||||||
i = 1
|
i = 1
|
||||||
while (x >= 1024 && i < 5) {
|
while (x >= 1024 && i < 5) {
|
||||||
@@ -505,14 +625,17 @@ file_stats()
|
|||||||
total += $1
|
total += $1
|
||||||
if (min == "" || $1 < min) min = $1
|
if (min == "" || $1 < min) min = $1
|
||||||
if (max == "" || $1 > max) max = $1
|
if (max == "" || $1 > max) max = $1
|
||||||
if ($1 == 0) bucket[0]++
|
# Histogram bucket b holds sizes in [1024^b, 1024^(b+1)); bucket 0
|
||||||
else {
|
# also takes empty files. Computed with an integer loop: a
|
||||||
b = int(log($1)/log(1024))
|
# log() ratio can fall just below an exact power of 1024.
|
||||||
bucket[b]++
|
v = $1; b = 0
|
||||||
}
|
while (v >= 1024) { v = int(v / 1024); b++ }
|
||||||
|
bucket[b]++
|
||||||
|
if (b > maxb) maxb = b
|
||||||
}
|
}
|
||||||
|
|
||||||
END {
|
END {
|
||||||
|
path = ENVIRON["FILE_STATS_PATH"]
|
||||||
count = NR
|
count = NR
|
||||||
if (count == 0) {
|
if (count == 0) {
|
||||||
print "No files found."
|
print "No files found."
|
||||||
@@ -548,8 +671,10 @@ file_stats()
|
|||||||
if (details) {
|
if (details) {
|
||||||
print "\nSize histogram:"
|
print "\nSize histogram:"
|
||||||
|
|
||||||
# Use a separate array for the loop to avoid collision
|
# Walk the buckets in ascending order (awk "for (b in array)" gives
|
||||||
for (b in bucket) {
|
# no defined order); empty buckets are skipped.
|
||||||
|
for (b = 0; b <= maxb; b++) {
|
||||||
|
if (!(b in bucket)) continue
|
||||||
# Pre-calculate label parts
|
# Pre-calculate label parts
|
||||||
# 1024^0 = 1 (B), 1024^1 = 1K, etc.
|
# 1024^0 = 1 (B), 1024^1 = 1K, etc.
|
||||||
low = (b == 0) ? 0 : (1024^b)
|
low = (b == 0) ? 0 : (1024^b)
|
||||||
@@ -559,8 +684,7 @@ file_stats()
|
|||||||
(b == 0) ? "0" : human_readable(low),
|
(b == 0) ? "0" : human_readable(low),
|
||||||
human_readable(high))
|
human_readable(high))
|
||||||
|
|
||||||
# We store buckets in an array, access them by index b
|
printf "%-25s : %6d files\n", label, bucket[b]
|
||||||
printf "%-25s : %6d fichiers\n", label, bucket[b]
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}'
|
}'
|
||||||
@@ -588,7 +712,10 @@ findbig()
|
|||||||
disp E "Invalid options, use \"findbig --help\" to display usage."
|
disp E "Invalid options, use \"findbig --help\" to display usage."
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
eval set -- "$PARSED"
|
# Restore the getopt-normalised (shell-quoted) argument list
|
||||||
|
local -a _pargs=()
|
||||||
|
_getopt_split _pargs "$PARSED"
|
||||||
|
set -- "${_pargs[@]}"
|
||||||
|
|
||||||
while true; do
|
while true; do
|
||||||
case "$1" in
|
case "$1" in
|
||||||
@@ -608,10 +735,6 @@ findbig()
|
|||||||
;;
|
;;
|
||||||
-l|--limit)
|
-l|--limit)
|
||||||
limit="$2"
|
limit="$2"
|
||||||
[[ "$limit" =~ ^[0-9]+$ ]] || {
|
|
||||||
disp E "Invalid limit: must be a positive integer."
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
shift 2
|
shift 2
|
||||||
;;
|
;;
|
||||||
-x|--one-fs)
|
-x|--one-fs)
|
||||||
@@ -629,7 +752,16 @@ findbig()
|
|||||||
esac
|
esac
|
||||||
done
|
done
|
||||||
|
|
||||||
|
# Checked here so that the configured default (FINDBIG_DEFAULT_LIMIT) is
|
||||||
|
# validated as well as -l.
|
||||||
|
if [[ ! "$limit" =~ ^[1-9][0-9]*$ ]]; then
|
||||||
|
disp E "Invalid limit '${limit}': must be a positive integer."
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
local dir="${1:-.}"
|
local dir="${1:-.}"
|
||||||
|
# "./" keeps a name starting with "-" from being read as a find option
|
||||||
|
[[ $dir == -* ]] && dir="./$dir"
|
||||||
|
|
||||||
# Prepare find arguments in an array for cleaner handling
|
# Prepare find arguments in an array for cleaner handling
|
||||||
local find_args=(-L "$dir")
|
local find_args=(-L "$dir")
|
||||||
@@ -637,12 +769,34 @@ findbig()
|
|||||||
find_args+=(-type f)
|
find_args+=(-type f)
|
||||||
|
|
||||||
# Logic: find files, print size and path, sort numeric reverse, take N
|
# Logic: find files, print size and path, sort numeric reverse, take N
|
||||||
if (( details )); then
|
local _fd
|
||||||
|
_fd=$(command -v fd 2>/dev/null || command -v fdfind 2>/dev/null)
|
||||||
|
|
||||||
|
if [[ -n "$_fd" ]]; then
|
||||||
|
local _fd_args=(--follow --no-ignore --hidden --absolute-path -t f)
|
||||||
|
(( one_fs )) && _fd_args+=(--one-file-system)
|
||||||
|
_fd_args+=(. "$dir")
|
||||||
|
if (( details )); then
|
||||||
|
local line
|
||||||
|
"$_fd" "${_fd_args[@]}" --exec stat --printf="%s %n\n" 2>/dev/null \
|
||||||
|
| sort -rn | head -n "$limit" \
|
||||||
|
| while IFS= read -r line; do
|
||||||
|
local path="${line#* }"
|
||||||
|
ls -ld -- "$path"
|
||||||
|
done
|
||||||
|
unset line
|
||||||
|
else
|
||||||
|
"$_fd" "${_fd_args[@]}" --exec stat --printf="%s %n\n" 2>/dev/null \
|
||||||
|
| sort -rn | head -n "$limit"
|
||||||
|
fi
|
||||||
|
elif (( details )); then
|
||||||
|
local line
|
||||||
find "${find_args[@]}" -printf "%s %p\n" 2>/dev/null | sort -rn | head -n "$limit" |
|
find "${find_args[@]}" -printf "%s %p\n" 2>/dev/null | sort -rn | head -n "$limit" |
|
||||||
while IFS= read -r line; do
|
while IFS= read -r line; do
|
||||||
local path="${line#* }"
|
local path="${line#* }"
|
||||||
ls -ld -- "$path"
|
ls -ld -- "$path"
|
||||||
done
|
done
|
||||||
|
unset line
|
||||||
else
|
else
|
||||||
find "${find_args[@]}" -printf "%s %p\n" 2>/dev/null | sort -rn | head -n "$limit"
|
find "${find_args[@]}" -printf "%s %p\n" 2>/dev/null | sort -rn | head -n "$limit"
|
||||||
fi
|
fi
|
||||||
@@ -671,7 +825,10 @@ findzero()
|
|||||||
disp E "Invalid options, use \"findzero --help\" to display usage."
|
disp E "Invalid options, use \"findzero --help\" to display usage."
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
eval set -- "$PARSED"
|
# Restore the getopt-normalised (shell-quoted) argument list
|
||||||
|
local -a _pargs=()
|
||||||
|
_getopt_split _pargs "$PARSED"
|
||||||
|
set -- "${_pargs[@]}"
|
||||||
|
|
||||||
while true; do
|
while true; do
|
||||||
case "$1" in
|
case "$1" in
|
||||||
@@ -708,12 +865,58 @@ findzero()
|
|||||||
done
|
done
|
||||||
|
|
||||||
local dir="${1:-.}"
|
local dir="${1:-.}"
|
||||||
local find_args=("-L" "$dir" "-type" "f" "-empty")
|
# "./" keeps a name starting with "-" from being read as a find option
|
||||||
|
[[ $dir == -* ]] && dir="./$dir"
|
||||||
|
|
||||||
|
# Symlinks are followed to report/count empty targets, but never when
|
||||||
|
# deleting: with -L, a link to an empty file matches and "delete" would
|
||||||
|
# remove the link itself.
|
||||||
|
local follow=1
|
||||||
|
(( delete )) && follow=0
|
||||||
|
|
||||||
|
local find_args=()
|
||||||
|
(( follow )) && find_args+=("-L")
|
||||||
|
find_args+=("$dir" "-type" "f" "-empty")
|
||||||
|
|
||||||
(( one_fs )) && find_args+=("-xdev")
|
(( one_fs )) && find_args+=("-xdev")
|
||||||
|
|
||||||
# Execution logic
|
# Execution logic
|
||||||
if (( delete )); then
|
local _fd
|
||||||
|
_fd=$(command -v fd 2>/dev/null || command -v fdfind 2>/dev/null)
|
||||||
|
|
||||||
|
if [[ -n "$_fd" ]]; then
|
||||||
|
local _fd_args=(--no-ignore --hidden --absolute-path -t f --size -1b)
|
||||||
|
(( follow )) && _fd_args+=(--follow)
|
||||||
|
(( one_fs )) && _fd_args+=(--one-file-system)
|
||||||
|
_fd_args+=(. "$dir")
|
||||||
|
# fd's "--size -1b" means "at most 1 byte", so it also lists files of
|
||||||
|
# exactly one byte. Every result is therefore re-checked with [[ -s ]]
|
||||||
|
# (true when the size is not zero) and skipped if not empty: deleting a
|
||||||
|
# one-byte file would be data loss.
|
||||||
|
if (( delete )); then
|
||||||
|
disp W "Deleting empty files in $dir..."
|
||||||
|
local f
|
||||||
|
"$_fd" "${_fd_args[@]}" 2>/dev/null | while IFS= read -r f; do
|
||||||
|
[[ -s "$f" ]] && continue
|
||||||
|
printf "%s\n" "$f"
|
||||||
|
rm -f -- "$f"
|
||||||
|
done
|
||||||
|
unset f
|
||||||
|
elif (( details )); then
|
||||||
|
local f
|
||||||
|
"$_fd" "${_fd_args[@]}" 2>/dev/null | while IFS= read -r f; do
|
||||||
|
[[ -s "$f" ]] && continue
|
||||||
|
ls -ls -- "$f"
|
||||||
|
done
|
||||||
|
unset f
|
||||||
|
else
|
||||||
|
local f
|
||||||
|
"$_fd" "${_fd_args[@]}" 2>/dev/null | while IFS= read -r f; do
|
||||||
|
if [[ ! -s "$f" ]]; then printf "%s\n" "$f"; fi
|
||||||
|
done
|
||||||
|
unset f
|
||||||
|
fi
|
||||||
|
elif (( delete )); then
|
||||||
disp W "Deleting empty files in $dir..."
|
disp W "Deleting empty files in $dir..."
|
||||||
find "${find_args[@]}" -delete -print
|
find "${find_args[@]}" -delete -print
|
||||||
elif (( details )); then
|
elif (( details )); then
|
||||||
@@ -745,7 +948,10 @@ finddead()
|
|||||||
disp E "Invalid options, use \"finddead --help\" to display usage."
|
disp E "Invalid options, use \"finddead --help\" to display usage."
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
eval set -- "$PARSED"
|
# Restore the getopt-normalised (shell-quoted) argument list
|
||||||
|
local -a _pargs=()
|
||||||
|
_getopt_split _pargs "$PARSED"
|
||||||
|
set -- "${_pargs[@]}"
|
||||||
|
|
||||||
while true; do
|
while true; do
|
||||||
case "$1" in
|
case "$1" in
|
||||||
@@ -782,12 +988,42 @@ finddead()
|
|||||||
done
|
done
|
||||||
|
|
||||||
local dir="${1:-.}"
|
local dir="${1:-.}"
|
||||||
|
# "./" keeps a name starting with "-" from being read as a find option
|
||||||
|
[[ $dir == -* ]] && dir="./$dir"
|
||||||
# -xtype l searches for links that do not point to an existing file
|
# -xtype l searches for links that do not point to an existing file
|
||||||
local find_args=("$dir" "-xtype" "l")
|
local find_args=("$dir" "-xtype" "l")
|
||||||
(( one_fs )) && find_args+=("-xdev")
|
(( one_fs )) && find_args+=("-xdev")
|
||||||
|
|
||||||
# Execution logic
|
# Execution logic
|
||||||
if (( delete )); then
|
local _fd
|
||||||
|
_fd=$(command -v fd 2>/dev/null || command -v fdfind 2>/dev/null)
|
||||||
|
|
||||||
|
if [[ -n "$_fd" ]]; then
|
||||||
|
# fd -t l lists all symlinks; post-filter broken ones (target unreachable via -e)
|
||||||
|
local _fd_args=(--no-ignore --hidden --absolute-path -t l)
|
||||||
|
(( one_fs )) && _fd_args+=(--one-file-system)
|
||||||
|
_fd_args+=(. "$dir")
|
||||||
|
if (( delete )); then
|
||||||
|
disp W "Deleting dead symlinks in $dir..."
|
||||||
|
local f
|
||||||
|
"$_fd" "${_fd_args[@]}" 2>/dev/null | while IFS= read -r f; do
|
||||||
|
if [[ ! -e "$f" ]]; then printf "%s\n" "$f"; rm -f -- "$f"; fi
|
||||||
|
done
|
||||||
|
unset f
|
||||||
|
elif (( details )); then
|
||||||
|
local f
|
||||||
|
"$_fd" "${_fd_args[@]}" 2>/dev/null | while IFS= read -r f; do
|
||||||
|
if [[ ! -e "$f" ]]; then ls -ls -- "$f"; fi
|
||||||
|
done
|
||||||
|
unset f
|
||||||
|
else
|
||||||
|
local f
|
||||||
|
"$_fd" "${_fd_args[@]}" 2>/dev/null | while IFS= read -r f; do
|
||||||
|
if [[ ! -e "$f" ]]; then printf "%s\n" "$f"; fi
|
||||||
|
done
|
||||||
|
unset f
|
||||||
|
fi
|
||||||
|
elif (( delete )); then
|
||||||
disp W "Deleting dead symlinks in $dir..."
|
disp W "Deleting dead symlinks in $dir..."
|
||||||
find "${find_args[@]}" -delete -print
|
find "${find_args[@]}" -delete -print
|
||||||
elif (( details )); then
|
elif (( details )); then
|
||||||
@@ -800,6 +1036,4 @@ export -f finddead
|
|||||||
# ------------------------------------------------------------------------------
|
# ------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
load_conf "filefct"
|
|
||||||
|
|
||||||
# EOF
|
# EOF
|
||||||
|
|||||||
+439
-23
@@ -34,12 +34,21 @@
|
|||||||
# * OF SUCH DAMAGE.
|
# * 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
|
# 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]
|
# Usage: busy [options] [pattern]
|
||||||
# Options:
|
# Options:
|
||||||
# --delay=<ms> : add a delay between each line output (milliseconds)
|
# -p, --pattern <hex> : the string to search for in the hexdump output
|
||||||
# pattern : the string to search for in the hexdump output (default is "ca fe")
|
# (default is "ca fe"); may also be given as argument
|
||||||
|
# -d, --delay <ms> : add a delay between each line output (milliseconds)
|
||||||
busy()
|
busy()
|
||||||
{
|
{
|
||||||
local pattern="${BUSY_DEFAULT_PATTERN:-ca fe}" delay_ms="${BUSY_DEFAULT_DELAY:-0}"
|
local pattern="${BUSY_DEFAULT_PATTERN:-ca fe}" delay_ms="${BUSY_DEFAULT_DELAY:-0}"
|
||||||
@@ -53,7 +62,10 @@ busy()
|
|||||||
disp E "Invalid options, use \"busy --help\" to display usage."
|
disp E "Invalid options, use \"busy --help\" to display usage."
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
eval set -- "$PARSED"
|
# Restore the getopt-normalised (shell-quoted) argument list
|
||||||
|
local -a _pargs=()
|
||||||
|
_getopt_split _pargs "$PARSED"
|
||||||
|
set -- "${_pargs[@]}"
|
||||||
|
|
||||||
while true; do
|
while true; do
|
||||||
case "$1" in
|
case "$1" in
|
||||||
@@ -63,7 +75,8 @@ busy()
|
|||||||
printf "Options:\n"
|
printf "Options:\n"
|
||||||
printf "\t-h, --help\t\tDisplay this help screen\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-p, --pattern PATTERN\tHex pattern to search (default: \"ca fe\")\n"
|
||||||
printf "\t-d, --delay MS\t\tDelay between matches in milliseconds\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
|
return 0
|
||||||
;;
|
;;
|
||||||
-p|--pattern)
|
-p|--pattern)
|
||||||
@@ -72,8 +85,307 @@ busy()
|
|||||||
;;
|
;;
|
||||||
-d|--delay)
|
-d|--delay)
|
||||||
delay_ms="$2"
|
delay_ms="$2"
|
||||||
if ! [[ "$delay_ms" =~ ^[0-9]+$ ]]; then
|
shift 2
|
||||||
disp E "Invalid delay: must be an integer (milliseconds)."
|
;;
|
||||||
|
--)
|
||||||
|
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
|
return 1
|
||||||
fi
|
fi
|
||||||
shift 2
|
shift 2
|
||||||
@@ -89,28 +401,132 @@ busy()
|
|||||||
esac
|
esac
|
||||||
done
|
done
|
||||||
|
|
||||||
# Convert milliseconds to seconds for 'sleep'
|
if ! _is_uint "$min_ms" || ! _is_uint "$max_ms"; then
|
||||||
local delay_s
|
disp E "min-delay and max-delay must be integers (milliseconds)."
|
||||||
delay_s=$(awk "BEGIN{
|
return 1
|
||||||
printf \"%.3f\", $delay_ms / 1000 }")
|
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
|
||||||
|
|
||||||
# Monitor /dev/urandom
|
|
||||||
(
|
(
|
||||||
hexdump -C < /dev/urandom | grep -iF --line-buffered "$pattern" | \
|
# Sleep for a random duration between min_ms and max_ms (RANDOM only
|
||||||
while read -r line; do
|
# spans 0..32767, which bounds the usable range).
|
||||||
echo "$line"
|
rand_sleep() {
|
||||||
[[ $delay_ms -gt 0 ]] && sleep "$delay_s"
|
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
|
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=$!
|
) & local sub_pid=$!
|
||||||
|
|
||||||
IFS= read -r -n 1 -s _ </dev/tty
|
_fun_stop_on_key "$sub_pid"
|
||||||
kill -- -"$sub_pid" 2>/dev/null || kill "$sub_pid" 2>/dev/null
|
|
||||||
wait "$sub_pid" 2>/dev/null
|
|
||||||
return 0
|
|
||||||
}
|
}
|
||||||
|
export -f hack
|
||||||
# ------------------------------------------------------------------------------
|
# ------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
load_conf "fun"
|
|
||||||
|
|
||||||
# EOF
|
# 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
|
||||||
+65
-9
@@ -34,55 +34,111 @@
|
|||||||
# * OF SUCH DAMAGE.
|
# * 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
|
# Display list of commands and general informations
|
||||||
# Usage: help
|
# Usage: help [command]
|
||||||
help()
|
help()
|
||||||
{
|
{
|
||||||
# shellcheck disable=SC2154 # color code in disp.sh
|
# With a command name, delegate to its own --help output. Only shell
|
||||||
# shellcheck disable=SC2059 # printf format is a color variable
|
# functions are accepted: running an arbitrary external program with an
|
||||||
printf "${BIWhite}Welcome to your profile! Here is a list of available commands:${DEFAULTCOL}\n\n"
|
# 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
|
||||||
|
|
||||||
|
# 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 "busy\t\tMonitor /dev/urandom for a hex pattern — look busy\n"
|
||||||
printf "check_updates\tCheck for new versions of profile\n"
|
printf "check_updates\tCheck for new versions of profile\n"
|
||||||
printf "clean\t\tErase backup files in given directories, optionally recursive\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 "disp\t\tDisplay formatted info/warning/error/debug messages\n"
|
||||||
printf "dwl\t\tDownload a URL using curl, wget, or fetch transparently\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 "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 "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 "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 "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 "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 "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 "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 "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 "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 "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 "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 "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 "meteo\t\tDisplay weather forecast for the configured or given city\n"
|
||||||
printf "myextip\t\tGet information about your public IP address\n"
|
printf "myextip\t\tGet information about your public IP address\n"
|
||||||
printf "pkgs\t\tSearch for a pattern in installed package names (dpkg/rpm, supports -i)\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 "ppg\t\tLook for the given pattern in running processes\n"
|
||||||
printf "ppn\t\tList processes matching an exact command name\n"
|
printf "ppn\t\tList processes matching an exact command name\n"
|
||||||
printf "ppu\t\tList processes owned by a specific user\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 "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 "pwdscore\tCalculate the strength score of a given password\n"
|
||||||
printf "rain\t\tConsole screensaver with falling-rain effect (multiple color themes)\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 "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 "rmspc\t\tReplace spaces in filenames with underscores (or a custom character)\n"
|
||||||
printf "setlocale\tSet console locale to any installed locale\n"
|
printf "setlocale\tSet console locale to any installed locale\n"
|
||||||
printf " * setc\tSet locale to standard C (POSIX)\n"
|
printf " * setc\tSet locale to standard C (POSIX)\n"
|
||||||
printf " * set*\tLocale shortcuts generated from SET_LOCALE in profile.conf\n"
|
printf " * set*\tLocale shortcuts generated from SET_LOCALE in profile.conf\n"
|
||||||
printf "settrace\tActivate or deactivate ERR trap to display backtrace on script errors\n"
|
|
||||||
printf "set_theme\tSwitch the prompt colour theme; no argument lists available themes\n"
|
printf "set_theme\tSwitch the prompt colour theme; no argument lists available themes\n"
|
||||||
printf "showinfo\tDisplay welcome banner and system information (figlet + neofetch/fastfetch)\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 "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 "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 "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 "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\n"
|
printf "ver\t\tDisplay the installed profile version\n"
|
||||||
|
|
||||||
printf "\nPlease use <command> --help to obtain usage details.\n"
|
printf "\nPlease use <command> --help or help <command> to obtain usage details.\n"
|
||||||
}
|
}
|
||||||
export -f help
|
export -f help
|
||||||
# ------------------------------------------------------------------------------
|
# ------------------------------------------------------------------------------
|
||||||
|
|||||||
+279
-21
@@ -34,6 +34,12 @@
|
|||||||
# * OF SUCH DAMAGE.
|
# * 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
|
# Show profile version
|
||||||
# Usage: ver
|
# Usage: ver
|
||||||
@@ -48,7 +54,10 @@ ver()
|
|||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
eval set -- "$PARSED"
|
# Restore the getopt-normalised (shell-quoted) argument list
|
||||||
|
local -a _pargs=()
|
||||||
|
_getopt_split _pargs "$PARSED"
|
||||||
|
set -- "${_pargs[@]}"
|
||||||
while true; do
|
while true; do
|
||||||
case "$1" in
|
case "$1" in
|
||||||
-h|--help)
|
-h|--help)
|
||||||
@@ -66,7 +75,7 @@ ver()
|
|||||||
esac
|
esac
|
||||||
done
|
done
|
||||||
|
|
||||||
[[ -z $PROFVERSION ]] && \
|
[[ -z ${PROFVERSION:-} ]] && \
|
||||||
disp W "No version defined. Profile is probably badly installed." && \
|
disp W "No version defined. Profile is probably badly installed." && \
|
||||||
return 1
|
return 1
|
||||||
disp "Profile version $PROFVERSION."
|
disp "Profile version $PROFVERSION."
|
||||||
@@ -88,7 +97,10 @@ meteo()
|
|||||||
disp E "Invalid options, use \"meteo --help\" to display usage."
|
disp E "Invalid options, use \"meteo --help\" to display usage."
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
eval set -- "$PARSED"
|
# Restore the getopt-normalised (shell-quoted) argument list
|
||||||
|
local -a _pargs=()
|
||||||
|
_getopt_split _pargs "$PARSED"
|
||||||
|
set -- "${_pargs[@]}"
|
||||||
while true; do
|
while true; do
|
||||||
case "$1" in
|
case "$1" in
|
||||||
-h|--help)
|
-h|--help)
|
||||||
@@ -127,6 +139,255 @@ 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
|
# Display system general information
|
||||||
# Usage: showinfo
|
# Usage: showinfo
|
||||||
@@ -140,7 +401,10 @@ showinfo()
|
|||||||
disp E "Invalid options, use \"showinfo --help\" to display usage."
|
disp E "Invalid options, use \"showinfo --help\" to display usage."
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
eval set -- "$PARSED"
|
# Restore the getopt-normalised (shell-quoted) argument list
|
||||||
|
local -a _pargs=()
|
||||||
|
_getopt_split _pargs "$PARSED"
|
||||||
|
set -- "${_pargs[@]}"
|
||||||
|
|
||||||
while true; do
|
while true; do
|
||||||
case "$1" in
|
case "$1" in
|
||||||
@@ -160,9 +424,13 @@ showinfo()
|
|||||||
esac
|
esac
|
||||||
done
|
done
|
||||||
|
|
||||||
local hostname_str
|
local hostname_str figopt=()
|
||||||
local figopt=()
|
# /proc first: the hostname binary is missing from minimal installations
|
||||||
hostname_str="$(hostname)"
|
if [[ -r /proc/sys/kernel/hostname ]]; then
|
||||||
|
read -r hostname_str < /proc/sys/kernel/hostname
|
||||||
|
else
|
||||||
|
hostname_str="${HOSTNAME:-$(hostname 2>/dev/null)}"
|
||||||
|
fi
|
||||||
|
|
||||||
printf "\n"
|
printf "\n"
|
||||||
if command -v figlet >/dev/null 2>&1; then
|
if command -v figlet >/dev/null 2>&1; then
|
||||||
@@ -174,26 +442,16 @@ showinfo()
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
printf "\n"
|
printf "\n"
|
||||||
if command -v neofetch >/dev/null 2>&1; then
|
if command -v fastfetch >/dev/null 2>&1; then
|
||||||
neofetch
|
|
||||||
elif command -v fastfetch >/dev/null 2>&1; then
|
|
||||||
fastfetch
|
fastfetch
|
||||||
|
elif command -v neofetch >/dev/null 2>&1; then
|
||||||
|
neofetch
|
||||||
else
|
else
|
||||||
(
|
pinfo "$@"
|
||||||
if [[ -s /etc/os-release ]]; then
|
|
||||||
# shellcheck disable=SC1091
|
|
||||||
. /etc/os-release
|
|
||||||
printf "%s %s\n" "$NAME" "$VERSION"
|
|
||||||
else
|
|
||||||
cat /proc/version
|
|
||||||
fi
|
|
||||||
printf "Uptime: %s\n" "$(uptime -p)"
|
|
||||||
)
|
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
export -f showinfo
|
export -f showinfo
|
||||||
# ------------------------------------------------------------------------------
|
# ------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
load_conf info
|
|
||||||
# EOF
|
# EOF
|
||||||
|
|||||||
+114
-16
@@ -34,14 +34,76 @@
|
|||||||
# * OF SUCH DAMAGE.
|
# * 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 lang
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
# 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()
|
locale_check()
|
||||||
{
|
{
|
||||||
locale -a | grep -qx "$1" || {
|
if [[ "${1:-}" == -h || "${1:-}" == --help ]]; then
|
||||||
disp W "Locale '$1' is not installed on this system."
|
printf "locale_check: tell whether a locale is installed.\n"
|
||||||
return 1
|
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
|
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
|
||||||
|
|
||||||
|
|
||||||
# ------------------------------------------------------------------------------
|
# ------------------------------------------------------------------------------
|
||||||
@@ -56,7 +118,10 @@ setlocale()
|
|||||||
disp E "Invalid options, use \"setlocale --help\" to display usage."
|
disp E "Invalid options, use \"setlocale --help\" to display usage."
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
eval set -- "$PARSED"
|
# Restore the getopt-normalised (shell-quoted) argument list
|
||||||
|
local -a _pargs=()
|
||||||
|
_getopt_split _pargs "$PARSED"
|
||||||
|
set -- "${_pargs[@]}"
|
||||||
while true; do
|
while true; do
|
||||||
case "$1" in
|
case "$1" in
|
||||||
-h|--help)
|
-h|--help)
|
||||||
@@ -80,7 +145,15 @@ setlocale()
|
|||||||
local loc=$1
|
local loc=$1
|
||||||
[[ -z $loc ]] && disp E "No locale specified." && return 1
|
[[ -z $loc ]] && disp E "No locale specified." && return 1
|
||||||
|
|
||||||
locale_check "$loc" || 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 LANG=$loc
|
||||||
export LC_MESSAGES=$loc
|
export LC_MESSAGES=$loc
|
||||||
@@ -101,8 +174,15 @@ export -f setlocale
|
|||||||
# Usage: setc
|
# Usage: setc
|
||||||
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
|
# Locale definitions
|
||||||
export LC_ALL=C
|
export LC_ALL=C
|
||||||
|
export LANG=C
|
||||||
disp I "Locale changed to standard C (POSIX)."
|
disp I "Locale changed to standard C (POSIX)."
|
||||||
}
|
}
|
||||||
export -f setc
|
export -f setc
|
||||||
@@ -158,25 +238,43 @@ build_locale_shortcuts()
|
|||||||
|
|
||||||
fname="set${alias}"
|
fname="set${alias}"
|
||||||
|
|
||||||
# Optional collision warning
|
# The generated function must never replace the real commands
|
||||||
if declare -F "$fname" >/dev/null 2>&1; then
|
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'."
|
disp W "Overriding existing function '$fname'."
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Build function dynamically
|
# Build function dynamically. %q quotes the locale so that nothing in
|
||||||
# shellcheck disable=SC2016
|
# the configuration value can be interpreted by eval. The shortcut
|
||||||
eval "${fname}() { setlocale \"$loc\"; }"
|
# 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
|
# shellcheck disable=SC2163
|
||||||
export -f "$fname"
|
export -f "$fname"
|
||||||
done
|
done
|
||||||
|
|
||||||
unset cfg item alias loc fname locale_items
|
unset cfg item alias loc fname locale_items qloc
|
||||||
}
|
}
|
||||||
export -f build_locale_shortcuts
|
|
||||||
# ------------------------------------------------------------------------------
|
# ------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
# Dynamically build locale functions based on SET_LOCALE configuration
|
||||||
load_conf lang
|
|
||||||
build_locale_shortcuts
|
build_locale_shortcuts
|
||||||
|
|
||||||
# ------------------------------------------------------------------------------
|
# ------------------------------------------------------------------------------
|
||||||
# EOF
|
# EOF
|
||||||
|
|||||||
+281
-77
@@ -34,29 +34,81 @@
|
|||||||
# * OF SUCH DAMAGE.
|
# * 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.
|
# Download a resource using curl, wget, or fetch.
|
||||||
# Usage: dwl <url> [output_file]
|
# Usage: dwl [-t <seconds>] [-r|--resume] <url> [output_file]
|
||||||
dwl()
|
dwl()
|
||||||
{
|
{
|
||||||
case "$1" in
|
local timeout=""
|
||||||
--help|-h)
|
local resume=0
|
||||||
echo "Usage: dwl <url> [output_file]"
|
|
||||||
echo "Downloads a resource using curl, wget, or fetch."
|
local _res="${DWL_DEFAULT_RESUME:-}"
|
||||||
echo ""
|
case "${_res,,}" in
|
||||||
echo "Arguments:"
|
1|true|yes|on)
|
||||||
echo " url The full URL to download (http/https/ftp)."
|
resume=1
|
||||||
echo " output_file (Optional) Path to save the file. If omitted, prints to stdout."
|
|
||||||
return 0
|
|
||||||
;;
|
|
||||||
"")
|
|
||||||
echo "Error: URL argument is missing." >&2
|
|
||||||
echo "Try 'get_resource --help' for usage." >&2
|
|
||||||
return 1
|
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
case "$1" in
|
# 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://*) ;;
|
http://*|https://*|ftp://*) ;;
|
||||||
*)
|
*)
|
||||||
echo "Error: '$1' does not look like a valid URL. Must start with http://, https://, or ftp://" >&2
|
echo "Error: '$1' does not look like a valid URL. Must start with http://, https://, or ftp://" >&2
|
||||||
@@ -65,63 +117,88 @@ dwl()
|
|||||||
esac
|
esac
|
||||||
|
|
||||||
local url="$1"
|
local url="$1"
|
||||||
local output="$2"
|
local output="${2:-}"
|
||||||
|
|
||||||
# Honour preferred tool from configuration; fall back to auto-detection.
|
# Honour preferred tool from configuration; fall back to auto-detection.
|
||||||
local preferred="${DWL_PREFERRED_TOOL:-}"
|
local preferred="${DWL_PREFERRED_TOOL:-}"
|
||||||
|
|
||||||
_try_curl()
|
_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 [[ -z "$output" ]]; then
|
||||||
curl -sL "$url"
|
if (( resume == 1 )); then
|
||||||
|
echo "Warning: --resume requires an output file; ignoring resume mode for stdout." >&2
|
||||||
|
fi
|
||||||
|
curl "${args[@]}" "$url"
|
||||||
else
|
else
|
||||||
curl -sL -o "$output" "$url"
|
(( resume == 1 )) && args+=(-C -)
|
||||||
|
curl "${args[@]}" -o "$output" "$url"
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
_try_wget()
|
_try_wget()
|
||||||
{
|
{
|
||||||
|
local args=(-q)
|
||||||
|
[[ -n "$timeout" ]] && args+=(--timeout="$timeout")
|
||||||
|
(( resume == 1 )) && args+=(-c)
|
||||||
if [[ -z "$output" ]]; then
|
if [[ -z "$output" ]]; then
|
||||||
wget -qO- "$url"
|
wget "${args[@]}" -O- "$url"
|
||||||
else
|
else
|
||||||
wget -q -O "$output" "$url"
|
wget "${args[@]}" -O "$output" "$url"
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
_try_fetch()
|
_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
|
if [[ -z "$output" ]]; then
|
||||||
fetch -o - "$url"
|
fetch "${args[@]}" -o - "$url"
|
||||||
else
|
else
|
||||||
fetch -o "$output" "$url"
|
fetch "${args[@]}" -o "$output" "$url"
|
||||||
fi
|
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 [[ -n "$preferred" ]]; then
|
||||||
command -v "$preferred" >/dev/null 2>&1 || {
|
if ! command -v "$preferred" >/dev/null 2>&1; then
|
||||||
echo "Error: preferred download tool '$preferred' is not installed." >&2
|
echo "Error: preferred download tool '$preferred' is not installed." >&2
|
||||||
|
unset -f _try_curl _try_wget _try_fetch
|
||||||
return 1
|
return 1
|
||||||
}
|
fi
|
||||||
case "$preferred" in
|
case "$preferred" in
|
||||||
curl) _try_curl ;;
|
curl|wget|fetch) tool="$preferred" ;;
|
||||||
wget) _try_wget ;;
|
|
||||||
fetch) _try_fetch ;;
|
|
||||||
*)
|
*)
|
||||||
echo "Error: DWL_PREFERRED_TOOL '$preferred' is not supported (use curl, wget or fetch)." >&2
|
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
|
return 1
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
elif command -v curl >/dev/null 2>&1; then
|
elif command -v curl >/dev/null 2>&1; then
|
||||||
_try_curl
|
tool=curl
|
||||||
elif command -v wget >/dev/null 2>&1; then
|
elif command -v wget >/dev/null 2>&1; then
|
||||||
_try_wget
|
tool=wget
|
||||||
elif command -v fetch >/dev/null 2>&1; then
|
elif command -v fetch >/dev/null 2>&1; then
|
||||||
_try_fetch
|
tool=fetch
|
||||||
else
|
else
|
||||||
echo "Error: No download utility (curl, wget, or fetch) found." >&2
|
echo "Error: No download utility (curl, wget, or fetch) found." >&2
|
||||||
|
unset -f _try_curl _try_wget _try_fetch
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
local rc=0
|
||||||
|
"_try_${tool}" || rc=$?
|
||||||
unset -f _try_curl _try_wget _try_fetch
|
unset -f _try_curl _try_wget _try_fetch
|
||||||
|
return "$rc"
|
||||||
}
|
}
|
||||||
export -f dwl
|
export -f dwl
|
||||||
# ------------------------------------------------------------------------------
|
# ------------------------------------------------------------------------------
|
||||||
@@ -138,30 +215,29 @@ isipv4()
|
|||||||
|
|
||||||
# Start with a regex format test (four octets)
|
# Start with a regex format test (four octets)
|
||||||
if [[ $ip =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]]; then
|
if [[ $ip =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]]; then
|
||||||
local old_ifs=$IFS
|
local -a ip_arr=()
|
||||||
IFS='.'
|
IFS='.' read -r -a ip_arr <<< "$ip"
|
||||||
read -r -a ip_arr <<< "$ip"
|
|
||||||
IFS=$old_ifs
|
|
||||||
|
|
||||||
# Ensure each octet is between 0 and 255
|
# Ensure each octet is between 0 and 255
|
||||||
local oct
|
local oct
|
||||||
for oct in "${ip_arr[@]}"; do
|
for oct in "${ip_arr[@]}"; do
|
||||||
# Reject leading plus/minus or empty entries
|
# Reject leading plus/minus or empty entries
|
||||||
if [[ -z $oct || $oct =~ [^0-9] ]]; then
|
if [[ -z $oct || $oct =~ [^0-9] ]]; then
|
||||||
[[ -t 1 ]] && disp "The given parameter is NOT a valid IPv4."
|
[[ -t 1 ]] && disp E "The given parameter is NOT a valid IPv4."
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
if (( oct > 255 )); then
|
# 10# forces base-10 so leading-zero octets (08, 09) are not read as octal
|
||||||
[[ -t 1 ]] && disp "The given parameter is NOT a valid IPv4."
|
if (( 10#$oct > 255 )); then
|
||||||
|
[[ -t 1 ]] && disp E "The given parameter is NOT a valid IPv4."
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
|
|
||||||
[[ -t 1 ]] && disp "The given IPv4 is valid."
|
[[ -t 1 ]] && disp I "The given IPv4 is valid."
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
[[ -t 1 ]] && disp "The given parameter is NOT a valid IPv4."
|
[[ -t 1 ]] && disp E "The given parameter is NOT a valid IPv4."
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
export -f isipv4
|
export -f isipv4
|
||||||
@@ -171,19 +247,94 @@ export -f isipv4
|
|||||||
# ------------------------------------------------------------------------------
|
# ------------------------------------------------------------------------------
|
||||||
# Determine if parameter is a valid IPv6 address
|
# Determine if parameter is a valid IPv6 address
|
||||||
# Usage: isipv6 <ip_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()
|
isipv6()
|
||||||
{
|
{
|
||||||
local ip="$1"
|
local ip="$1"
|
||||||
local regex='^([0-9a-fA-F]{0,4}:){1,7}[0-9a-fA-F]{0,4}$'
|
local valid=0
|
||||||
if [[ $ip =~ $regex ]]; then
|
|
||||||
if [[ -t 1 ]]; then
|
# A single-pass validation; any failing check breaks out with valid=0.
|
||||||
disp "The given IPv6 is valid."
|
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
|
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
|
return 0
|
||||||
fi
|
fi
|
||||||
if [[ -t 1 ]]; then
|
[[ -t 1 ]] && disp E "The given parameter is not a valid IPv6."
|
||||||
disp "The given parameter is not a valid IPv6."
|
|
||||||
fi
|
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
export -f isipv6
|
export -f isipv6
|
||||||
@@ -191,19 +342,24 @@ export -f isipv6
|
|||||||
|
|
||||||
|
|
||||||
# ------------------------------------------------------------------------------
|
# ------------------------------------------------------------------------------
|
||||||
# Encode a string so it can be used as a URL parameter
|
# 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>
|
# Usage: urlencode <string>
|
||||||
urlencode()
|
urlencode()
|
||||||
{
|
{
|
||||||
local LANG=C
|
# 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 str="$*"
|
||||||
local length="${#str}"
|
local length="${#str}"
|
||||||
|
local i
|
||||||
for (( i = 0; i < length; i++ )); do
|
for (( i = 0; i < length; i++ )); do
|
||||||
local c="${str:i:1}"
|
local c="${str:i:1}"
|
||||||
case "$c" in
|
case "$c" in
|
||||||
[a-zA-Z0-9.~_-]) printf '%s' "$c" ;;
|
[a-zA-Z0-9.~_-]) printf '%s' "$c" ;;
|
||||||
' ') printf '+' ;;
|
*) printf '%%%02X' "'$c" ;;
|
||||||
*) printf '%%%02X' "'$c" #| cut -d' ' -f2 ;;
|
|
||||||
esac
|
esac
|
||||||
done
|
done
|
||||||
}
|
}
|
||||||
@@ -281,42 +437,90 @@ myextip()
|
|||||||
shift
|
shift
|
||||||
done
|
done
|
||||||
|
|
||||||
# Fetch data. Allow overriding endpoint via MYEXTIP_DEFAULT_URL config key.
|
# Fetch data. The endpoint can be changed with the MYEXTIP_DEFAULT_URL
|
||||||
local api_url="${MYEXTIP_DEFAULT_URL:-https://ip-api.com/json/}"
|
# 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
|
local response
|
||||||
if ! response=$(dwl "$api_url"); then
|
if ! response=$(dwl -t 10 "$api_url"); then
|
||||||
disp E "Failed to fetch external IP information from $api_url"
|
disp E "Failed to fetch external IP information from $api_url"
|
||||||
return 2
|
return 2
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Parse with jq when available and when raw wasn't requested. The jq filter
|
if [[ "$show_raw" == true ]]; then
|
||||||
# is tolerant to field-name differences between providers (ip-api / ipinfo).
|
printf '%s\n' "$response"
|
||||||
if command -v jq >/dev/null 2>&1 && [[ "$show_raw" != true ]]; then
|
return 0
|
||||||
echo "$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" '
|
|
||||||
[
|
|
||||||
(if $all or $ip then "IP Address : \(.query // .ip)" else empty end),
|
|
||||||
(if $all or $isp then "ISP : \(.isp // .org)" else empty end),
|
|
||||||
(if $all or $loc then
|
|
||||||
("Location : " + ((.city // "") + (if .city then ", " else "" end) + (if .regionName then .regionName else .region end) + (if .country then ", " + .country else "" end)))
|
|
||||||
else empty end),
|
|
||||||
(if $all or $coord then (if (.lat and .lon) then "Coordinates: \(.lat), \(.lon)" elif .loc then "Coordinates: \(.loc)" else empty end) else empty end),
|
|
||||||
(if $all or $as then "AS : \(.as // .org)" else empty end)
|
|
||||||
] | .[]'
|
|
||||||
else
|
|
||||||
[[ "$show_raw" != true ]] && disp W "jq is not installed, displaying raw JSON response."
|
|
||||||
echo "$response"
|
|
||||||
fi
|
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
|
export -f myextip
|
||||||
# ------------------------------------------------------------------------------
|
# ------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
# ------------------------------------------------------------------------------
|
|
||||||
# EOF
|
|
||||||
|
|
||||||
load_conf "net"
|
|
||||||
|
|
||||||
# EOF
|
# EOF
|
||||||
|
|||||||
+400
-20
@@ -34,14 +34,28 @@
|
|||||||
# * OF SUCH DAMAGE.
|
# * OF SUCH DAMAGE.
|
||||||
# ------------------------------------------------------------------------------
|
# ------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
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.
|
# Detect the active package manager of the current distribution.
|
||||||
# Detection is based on /etc/os-release (ID / ID_LIKE), then falls back to
|
# Detection is based on /etc/os-release (ID / ID_LIKE), then falls back to
|
||||||
# checking available binaries in a fixed priority order.
|
# checking available binaries in a fixed priority order.
|
||||||
# Echoes one of: apt dnf yum zypper pacman apk portage xbps nix
|
# Echoes one of: apt dnf yum zypper pacman apk portage xbps nix
|
||||||
# Returns 1 if no known package manager could be identified.
|
# Returns 1 if no known package manager could be identified.
|
||||||
_get_pkgmgr()
|
# shellcheck disable=SC2120 # only --help is a possible argument
|
||||||
|
get_pkgmgr()
|
||||||
{
|
{
|
||||||
|
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=""
|
local distro_id="" distro_like=""
|
||||||
if [[ -r /etc/os-release ]]; then
|
if [[ -r /etc/os-release ]]; then
|
||||||
# shellcheck disable=SC1091
|
# shellcheck disable=SC1091
|
||||||
@@ -56,30 +70,48 @@ _get_pkgmgr()
|
|||||||
for id in $distro_id $distro_like; do
|
for id in $distro_id $distro_like; do
|
||||||
case "${id,,}" in
|
case "${id,,}" in
|
||||||
debian|ubuntu|linuxmint|raspbian|pop|kali|elementary|zorin|neon|parrot)
|
debian|ubuntu|linuxmint|raspbian|pop|kali|elementary|zorin|neon|parrot)
|
||||||
echo "apt"; return 0 ;;
|
echo "apt"
|
||||||
|
return 0
|
||||||
|
;;
|
||||||
fedora)
|
fedora)
|
||||||
echo "dnf"; return 0 ;;
|
echo "dnf"
|
||||||
|
return 0
|
||||||
|
;;
|
||||||
rhel|centos|rocky|almalinux|ol|scientific|amzn)
|
rhel|centos|rocky|almalinux|ol|scientific|amzn)
|
||||||
command -v dnf >/dev/null 2>&1 && { echo "dnf"; return 0; }
|
command -v dnf >/dev/null 2>&1 && { echo "dnf"; return 0; }
|
||||||
echo "yum"; return 0 ;;
|
echo "yum"
|
||||||
opensuse*|sles|sled)
|
return 0
|
||||||
echo "zypper"; return 0 ;;
|
;;
|
||||||
|
opensuse*|suse|sles|sled)
|
||||||
|
echo "zypper"
|
||||||
|
return 0
|
||||||
|
;;
|
||||||
arch|manjaro|endeavouros|garuda|artix|cachyos)
|
arch|manjaro|endeavouros|garuda|artix|cachyos)
|
||||||
echo "pacman"; return 0 ;;
|
echo "pacman"
|
||||||
|
return 0
|
||||||
|
;;
|
||||||
alpine)
|
alpine)
|
||||||
echo "apk"; return 0 ;;
|
echo "apk"
|
||||||
|
return 0
|
||||||
|
;;
|
||||||
gentoo)
|
gentoo)
|
||||||
echo "portage"; return 0 ;;
|
echo "portage"
|
||||||
|
return 0
|
||||||
|
;;
|
||||||
void)
|
void)
|
||||||
echo "xbps"; return 0 ;;
|
echo "xbps"
|
||||||
|
return 0
|
||||||
|
;;
|
||||||
nixos)
|
nixos)
|
||||||
echo "nix"; return 0 ;;
|
echo "nix"
|
||||||
|
return 0
|
||||||
|
;;
|
||||||
esac
|
esac
|
||||||
done
|
done
|
||||||
|
|
||||||
# Fallback: check for binaries in priority order.
|
# Fallback: check for binaries in priority order.
|
||||||
local bin
|
local bin
|
||||||
for bin in apt-get dnf yum zypper pacman apk emerge xbps-install nix-env; do
|
for bin in apt apt-get dnf yum zypper pacman apk emerge xbps-install nix-env; do
|
||||||
command -v "$bin" >/dev/null 2>&1 && {
|
command -v "$bin" >/dev/null 2>&1 && {
|
||||||
case "$bin" in
|
case "$bin" in
|
||||||
apt-get) echo "apt" ;;
|
apt-get) echo "apt" ;;
|
||||||
@@ -94,10 +126,7 @@ _get_pkgmgr()
|
|||||||
|
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
export -f _get_pkgmgr
|
export -f get_pkgmgr
|
||||||
# ------------------------------------------------------------------------------
|
|
||||||
|
|
||||||
|
|
||||||
# ------------------------------------------------------------------------------
|
# ------------------------------------------------------------------------------
|
||||||
# Look for a package within installed one
|
# Look for a package within installed one
|
||||||
# Usage: pkgs <string>
|
# Usage: pkgs <string>
|
||||||
@@ -112,7 +141,9 @@ pkgs()
|
|||||||
disp E "Invalid options, use \"pkgs --help\" to display usage."
|
disp E "Invalid options, use \"pkgs --help\" to display usage."
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
eval set -- "$PARSED"
|
local -a _pargs=()
|
||||||
|
_getopt_split _pargs "$PARSED"
|
||||||
|
set -- "${_pargs[@]}"
|
||||||
|
|
||||||
while true; do
|
while true; do
|
||||||
case "$1" in
|
case "$1" in
|
||||||
@@ -150,7 +181,7 @@ pkgs()
|
|||||||
(( ignore_case )) && grep_opt="-i"
|
(( ignore_case )) && grep_opt="-i"
|
||||||
|
|
||||||
local pkgmgr
|
local pkgmgr
|
||||||
pkgmgr=$(_get_pkgmgr) || {
|
pkgmgr=$(get_pkgmgr) || {
|
||||||
disp E "No usable package manager could be detected on this system."
|
disp E "No usable package manager could be detected on this system."
|
||||||
return 2
|
return 2
|
||||||
}
|
}
|
||||||
@@ -170,12 +201,361 @@ pkgs()
|
|||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
"${list_cmd[@]}" | grep ${grep_opt:+"$grep_opt"} "$pkg"
|
# -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
|
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
|
||||||
|
|
||||||
load_conf "packages"
|
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
|
# EOF
|
||||||
|
|||||||
+338
-81
@@ -1,6 +1,6 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# ------------------------------------------------------------------------------
|
# ------------------------------------------------------------------------------
|
||||||
# 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.
|
# Protected by the BSD3 license. Please read bellow for details.
|
||||||
#
|
#
|
||||||
# * Redistribution and use in source and binary forms,
|
# * Redistribution and use in source and binary forms,
|
||||||
@@ -34,18 +34,50 @@
|
|||||||
# * OF SUCH DAMAGE.
|
# * 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
|
# Search processes matching the given string
|
||||||
# Usage: ppg <string>
|
# Usage: ppg <string>
|
||||||
ppg()
|
ppg()
|
||||||
{
|
{
|
||||||
if [[ "$1" == "-h" || "$1" == "--help" ]]; then
|
local PARSED
|
||||||
printf "ppg: Search processes matching the given string.\n\n"
|
|
||||||
printf "Usage: ppg <string>\n\n"
|
PARSED=$(getopt -o h --long help -n 'ppg' -- "$@")
|
||||||
printf "Options:\n"
|
# shellcheck disable=SC2181 # getopt return code is checked immediately after
|
||||||
printf "\t-h, --help\t\tDisplay this help screen\n"
|
if [[ $? -ne 0 ]]; then
|
||||||
return 0
|
disp E "Invalid options, use \"ppg --help\" to display usage."
|
||||||
|
return 1
|
||||||
fi
|
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
|
if [[ -z "$1" ]]; then
|
||||||
disp E "Usage: ppg <string>"
|
disp E "Usage: ppg <string>"
|
||||||
return 1
|
return 1
|
||||||
@@ -53,12 +85,17 @@ ppg()
|
|||||||
|
|
||||||
local pattern="$*"
|
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
|
if command -v pgrep >/dev/null 2>&1; then
|
||||||
pgrep -af -- "$pattern"
|
pgrep -af -- "$pattern"
|
||||||
return $?
|
return $?
|
||||||
fi
|
fi
|
||||||
|
|
||||||
ps -ef | awk -v pattern="$pattern" '
|
ps -ef | PPG_PATTERN="$pattern" awk '
|
||||||
|
BEGIN { pattern = ENVIRON["PPG_PATTERN"] }
|
||||||
NR == 1 {
|
NR == 1 {
|
||||||
print
|
print
|
||||||
next
|
next
|
||||||
@@ -73,21 +110,43 @@ ppg()
|
|||||||
'
|
'
|
||||||
}
|
}
|
||||||
export -f ppg
|
export -f ppg
|
||||||
# ------------------------------------------------------------------------------
|
|
||||||
|
|
||||||
|
|
||||||
# ------------------------------------------------------------------------------
|
# ------------------------------------------------------------------------------
|
||||||
# List processes owned by a specific user
|
# List processes owned by a specific user
|
||||||
# Usage: ppu <username>
|
# Usage: ppu <username>
|
||||||
ppu()
|
ppu()
|
||||||
{
|
{
|
||||||
if [[ "$1" == "-h" || "$1" == "--help" ]]; then
|
local PARSED
|
||||||
printf "ppu: List processes owned by a specific user.\n\n"
|
|
||||||
printf "Usage: ppu <username>\n\n"
|
PARSED=$(getopt -o h --long help -n 'ppu' -- "$@")
|
||||||
printf "Options:\n"
|
# shellcheck disable=SC2181 # getopt return code is checked immediately after
|
||||||
printf "\t-h, --help\t\tDisplay this help screen\n"
|
if [[ $? -ne 0 ]]; then
|
||||||
return 0
|
disp E "Invalid options, use \"ppu --help\" to display usage."
|
||||||
|
return 1
|
||||||
fi
|
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
|
if [[ -z "$1" ]]; then
|
||||||
disp E "Usage: ppu <username>"
|
disp E "Usage: ppu <username>"
|
||||||
return 1
|
return 1
|
||||||
@@ -98,48 +157,101 @@ ppu()
|
|||||||
ps -u "$1" -o "${PPU_DEFAULT_FORMAT:-pid,user,%cpu,%mem,start,time,command}"
|
ps -u "$1" -o "${PPU_DEFAULT_FORMAT:-pid,user,%cpu,%mem,start,time,command}"
|
||||||
}
|
}
|
||||||
export -f ppu
|
export -f ppu
|
||||||
# ------------------------------------------------------------------------------
|
|
||||||
|
|
||||||
|
|
||||||
# ------------------------------------------------------------------------------
|
# ------------------------------------------------------------------------------
|
||||||
# List processes by exact command name (no path/parameters)
|
# List processes by exact command name (no path/parameters)
|
||||||
# Usage: ppn <command_name>
|
# Usage: ppn <command_name>
|
||||||
ppn()
|
ppn()
|
||||||
{
|
{
|
||||||
if [[ "$1" == "-h" || "$1" == "--help" ]]; then
|
local PARSED
|
||||||
printf "ppn: List processes by exact command name (no path/parameters).\n\n"
|
|
||||||
printf "Usage: ppn <command_name>\n\n"
|
PARSED=$(getopt -o h --long help -n 'ppn' -- "$@")
|
||||||
printf "Options:\n"
|
# shellcheck disable=SC2181 # getopt return code is checked immediately after
|
||||||
printf "\t-h, --help\t\tDisplay this help screen\n"
|
if [[ $? -ne 0 ]]; then
|
||||||
return 0
|
disp E "Invalid options, use \"ppn --help\" to display usage."
|
||||||
|
return 1
|
||||||
fi
|
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
|
if [[ -z "$1" ]]; then
|
||||||
disp E "Usage: ppn <command_name>"
|
disp E "Usage: ppn <command_name>"
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# -e: select all processes
|
# ps prints the PID and the command name (comm, 15 characters at most, no
|
||||||
# -o: specify custom output columns (PID and Command name)
|
# arguments); awk keeps the header and the lines whose whole name equals
|
||||||
# grep -w: ensures exact word matching so 'bash' doesn't match 'dbash'
|
# the argument. grep -w was wrong: it also matched inside the PID column,
|
||||||
# shellcheck disable=SC2009 # pgrep do not offer the -w switch
|
# and in names such as "kde-bash" ("-" is not a word character).
|
||||||
ps -eo pid,comm | grep -w "$1"
|
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
|
export -f ppn
|
||||||
# ------------------------------------------------------------------------------
|
|
||||||
|
|
||||||
|
|
||||||
# ------------------------------------------------------------------------------
|
# ------------------------------------------------------------------------------
|
||||||
# Get PID list of the given process name
|
# Get PID list of the given process name
|
||||||
# Usage: ppid <process_name [process_name2 ...]>
|
# Usage: gpid <process_name [process_name2 ...]>
|
||||||
gpid()
|
gpid()
|
||||||
{
|
{
|
||||||
if [[ "$1" == "-h" || "$1" == "--help" ]]; then
|
local PARSED
|
||||||
printf "gpid: Get PID list of the given process name.\n\n"
|
|
||||||
printf "Usage: gpid <process_name [process_name2 ...]>\n\n"
|
PARSED=$(getopt -o h --long help -n 'gpid' -- "$@")
|
||||||
printf "Options:\n"
|
# shellcheck disable=SC2181 # getopt return code is checked immediately after
|
||||||
printf "\t-h, --help\t\tDisplay this help screen\n"
|
if [[ $? -ne 0 ]]; then
|
||||||
return 0
|
disp E "Invalid options, use \"gpid --help\" to display usage."
|
||||||
|
return 1
|
||||||
fi
|
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
|
if [[ -z "$1" ]]; then
|
||||||
disp E "Usage: gpid <process_name [process_name2 ...]>"
|
disp E "Usage: gpid <process_name [process_name2 ...]>"
|
||||||
return 1
|
return 1
|
||||||
@@ -156,7 +268,8 @@ gpid()
|
|||||||
if command -v pgrep >/dev/null 2>&1; then
|
if command -v pgrep >/dev/null 2>&1; then
|
||||||
result=$(pgrep -d ' ' -x -- "$proc_name")
|
result=$(pgrep -d ' ' -x -- "$proc_name")
|
||||||
else
|
else
|
||||||
result=$(ps -eo pid=,comm= | awk -v proc="$proc_name" '
|
result=$(ps -eo pid=,comm= | GPID_NAME="$proc_name" awk '
|
||||||
|
BEGIN { proc = ENVIRON["GPID_NAME"] }
|
||||||
$2 == proc {
|
$2 == proc {
|
||||||
if (out != "") {
|
if (out != "") {
|
||||||
out = out " "
|
out = out " "
|
||||||
@@ -182,75 +295,219 @@ gpid()
|
|||||||
(( found )) || return 1
|
(( found )) || return 1
|
||||||
}
|
}
|
||||||
export -f gpid
|
export -f gpid
|
||||||
# ------------------------------------------------------------------------------
|
|
||||||
|
|
||||||
|
|
||||||
# ------------------------------------------------------------------------------
|
# ------------------------------------------------------------------------------
|
||||||
# Kill all processes owned by the given users (kill user)
|
# Kill all processes owned by the given users (kill user)
|
||||||
# Usage: ku <username1 [username2 ...]>
|
# 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()
|
ku()
|
||||||
{
|
{
|
||||||
if [[ "$1" == "-h" || "$1" == "--help" ]]; then
|
local dry_run=0
|
||||||
printf "ku: Kill all processes owned by the given users.\n\n"
|
local -a signal_opt=() _ku_sigs=() _ku_rest=()
|
||||||
printf "Usage: ku <username1 [username2 ...]>\n\n"
|
|
||||||
printf "Options:\n"
|
_proc_split_signals _ku_sigs _ku_rest "$@"
|
||||||
printf "\t-h, --help\t\tDisplay this help screen\n"
|
|
||||||
return 0
|
local PARSED
|
||||||
fi
|
PARSED=$(getopt -o hns: --long help,dry-run,signal: -n 'ku' -- "${_ku_rest[@]}")
|
||||||
if [[ -z "$1" ]]; then
|
# shellcheck disable=SC2181 # getopt return code is checked immediately after
|
||||||
disp E "Usage: ku <username1 [username2 ...]>"
|
if [[ $? -ne 0 ]]; then
|
||||||
|
disp E "Invalid options, use \"ku --help\" to display usage."
|
||||||
return 1
|
return 1
|
||||||
fi
|
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
|
for u in "$@"; do
|
||||||
if ! id "$u" >/dev/null 2>&1; then
|
if ! uid=$(id -u -- "$u" 2>/dev/null); then
|
||||||
disp E "User '$u' does not exist."
|
disp E "User '$u' does not exist."
|
||||||
return 1
|
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
|
else
|
||||||
killall ${KU_DEFAULT_SIGNAL:+-${KU_DEFAULT_SIGNAL}} -u "$u"
|
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
|
fi
|
||||||
done
|
done
|
||||||
|
return "$rc"
|
||||||
}
|
}
|
||||||
export -f ku
|
export -f ku
|
||||||
# ------------------------------------------------------------------------------
|
|
||||||
|
|
||||||
|
|
||||||
# ------------------------------------------------------------------------------
|
# ------------------------------------------------------------------------------
|
||||||
# Kill all children of a process then the process (kill tree)
|
# Kill all children of a process then the process (kill tree)
|
||||||
# Usage: kt <pid> [kill_options]
|
# Usage: kt [options] <pid>
|
||||||
kt()
|
kt()
|
||||||
{
|
{
|
||||||
if [[ "$1" == "-h" || "$1" == "--help" ]]; then
|
local dry_run=0
|
||||||
printf "kt: Kill all children of a process then the process (kill tree).\n\n"
|
local -a kill_args=() _kt_sigs=() _kt_rest=()
|
||||||
printf "Usage: kt <pid> [kill_options]\n\n"
|
|
||||||
printf "Options:\n"
|
_proc_split_signals _kt_sigs _kt_rest "$@"
|
||||||
printf "\t-h, --help\t\tDisplay this help screen\n"
|
|
||||||
return 0
|
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
|
fi
|
||||||
if [[ -z "$1" ]]; then
|
# Restore the getopt-normalised (shell-quoted) argument list
|
||||||
disp E "Usage: kt <pid>"
|
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
|
return 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
local parent_pid="$1"
|
local parent_pid="$1"
|
||||||
shift
|
if [[ ! $parent_pid =~ ^[0-9]+$ ]]; then
|
||||||
|
disp E "Invalid PID: '$parent_pid'."
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
if [[ "$parent_pid" == "0" || "$parent_pid" == "1" ]]; then
|
if [[ "$parent_pid" == "0" || "$parent_pid" == "1" ]]; then
|
||||||
disp E "Safety abort: Refusing to kill PID $parent_pid (system critical)."
|
disp E "Safety abort: Refusing to kill PID $parent_pid (system critical)."
|
||||||
return 1
|
return 1
|
||||||
fi
|
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
|
||||||
|
|
||||||
local children_pids
|
_kt_tree "$dry_run" "$parent_pid" "${kill_args[@]}"
|
||||||
children_pids=$(pgrep -P "$parent_pid")
|
|
||||||
|
|
||||||
for pid in $children_pids; do
|
|
||||||
kt "$pid" "$@" || break
|
|
||||||
done
|
|
||||||
kill "$@" "$parent_pid"
|
|
||||||
}
|
}
|
||||||
export -f kt
|
export -f kt
|
||||||
# ------------------------------------------------------------------------------
|
# ------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
load_conf "processes"
|
|
||||||
|
|
||||||
# EOF
|
# EOF
|
||||||
|
|
||||||
|
|||||||
+219
-115
@@ -34,6 +34,12 @@
|
|||||||
# * OF SUCH DAMAGE.
|
# * OF SUCH DAMAGE.
|
||||||
# ------------------------------------------------------------------------------
|
# ------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
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.
|
# Parse a prompt theme file safely — it is NEVER sourced or executed.
|
||||||
@@ -41,11 +47,8 @@
|
|||||||
# PROMPT_COLOR_* — prompt slot colours (TIME_FG, BAR_BG, …)
|
# PROMPT_COLOR_* — prompt slot colours (TIME_FG, BAR_BG, …)
|
||||||
# Standard colour variables from disp.sh (Blue, On_IBlack, …) — allows a
|
# Standard colour variables from disp.sh (Blue, On_IBlack, …) — allows a
|
||||||
# theme to redefine the palette used everywhere in the shell session.
|
# theme to redefine the palette used everywhere in the shell session.
|
||||||
# Allowed value forms:
|
# Values follow the forms accepted by _prompt_resolve_color. Any other key or
|
||||||
# $ColorName or ${ColorName} — colour variable from disp.sh (resolved by
|
# value is rejected with a warning.
|
||||||
# indirection via ${!varname})
|
|
||||||
# \e[...m or \033[...m — raw ANSI escape literal (single block)
|
|
||||||
# Any other key or value is rejected with a warning.
|
|
||||||
# Usage: load_theme <theme_name_or_path> [theme_dir]
|
# Usage: load_theme <theme_name_or_path> [theme_dir]
|
||||||
# theme_name_or_path : bare name (e.g. "dark") or an explicit path.
|
# theme_name_or_path : bare name (e.g. "dark") or an explicit path.
|
||||||
# theme_dir : directory to search for bare names; defaults to
|
# theme_dir : directory to search for bare names; defaults to
|
||||||
@@ -54,23 +57,18 @@
|
|||||||
load_theme()
|
load_theme()
|
||||||
{
|
{
|
||||||
local theme_name="$1"
|
local theme_name="$1"
|
||||||
local theme_dir="${2:-${PROMPT_THEME_DIR:-$MYPATH/profile.d/themes}}"
|
|
||||||
local theme_file=""
|
local theme_file=""
|
||||||
|
|
||||||
[[ -z "$theme_name" ]] && return 0
|
[[ -z "$theme_name" ]] && return 0
|
||||||
|
|
||||||
if [[ "$theme_name" == /* || "$theme_name" == */* ]]; then
|
theme_file=$(_prompt_theme_file "$theme_name" "${2:-}")
|
||||||
theme_file="$theme_name"
|
|
||||||
else
|
|
||||||
theme_file="$theme_dir/${theme_name}.theme"
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ ! -f "$theme_file" || ! -r "$theme_file" ]]; then
|
if [[ ! -f "$theme_file" || ! -r "$theme_file" ]]; then
|
||||||
printf "[ Warning ] load_theme: theme file not found: %s\n" "$theme_file" >&2
|
printf "[ Warning ] load_theme: theme file not found: %s\n" "$theme_file" >&2
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# ---- Key whitelist: prompt slots ----------------------------------------
|
# ---- Key whitelist: prompt slots and standard colour variables ----------
|
||||||
local -A _lth_allowed=(
|
local -A _lth_allowed=(
|
||||||
[PROMPT_COLOR_TIME_FG]=1 [PROMPT_COLOR_TIME_BG]=1
|
[PROMPT_COLOR_TIME_FG]=1 [PROMPT_COLOR_TIME_BG]=1
|
||||||
[PROMPT_COLOR_BAR_BG]=1
|
[PROMPT_COLOR_BAR_BG]=1
|
||||||
@@ -78,49 +76,22 @@ load_theme()
|
|||||||
[PROMPT_COLOR_ERR_BG]=1 [PROMPT_COLOR_ERR_FG]=1 [PROMPT_COLOR_ERR_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_ROOT_FG]=1 [PROMPT_COLOR_USER_FG]=1
|
||||||
[PROMPT_COLOR_DIR_FG]=1
|
[PROMPT_COLOR_DIR_FG]=1
|
||||||
|
[PROMPT_COLOR_CTX_FG]=1
|
||||||
)
|
)
|
||||||
|
|
||||||
# ---- Colour variable names exported by disp.sh --------------------------
|
|
||||||
local _lth_color_re
|
|
||||||
_lth_color_re='Black|Red|Green|Yellow|Blue|Purple|Cyan|White'
|
|
||||||
_lth_color_re+='|BBlack|BRed|BGreen|BYellow|BBlue|BPurple|BCyan|BWhite'
|
|
||||||
_lth_color_re+='|UBlack|URed|UGreen|UYellow|UBlue|UPurple|UCyan|UWhite'
|
|
||||||
_lth_color_re+='|On_Black|On_Red|On_Green|On_Yellow|On_Blue|On_Purple|On_Cyan|On_White'
|
|
||||||
_lth_color_re+='|IBlack|IRed|IGreen|IYellow|IBlue|IPurple|ICyan|IWhite'
|
|
||||||
_lth_color_re+='|BIBlack|BIRed|BIGreen|BIYellow|BIBlue|BIPurple|BICyan|BIWhite'
|
|
||||||
_lth_color_re+='|On_IBlack|On_IRed|On_IGreen|On_IYellow|On_IBlue|On_IPurple|On_ICyan|On_IWhite'
|
|
||||||
_lth_color_re+='|DEFAULTFG|DEFAULTBG|DEFAULTCOL|RESETCOL'
|
|
||||||
|
|
||||||
# ---- Key whitelist: standard colour vars (same list as above) -----------
|
|
||||||
local _lth_cn
|
local _lth_cn
|
||||||
for _lth_cn in \
|
for _lth_cn in "${_PROMPT_COLOR_NAMES[@]}"; do
|
||||||
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; do
|
|
||||||
_lth_allowed[$_lth_cn]=1
|
_lth_allowed[$_lth_cn]=1
|
||||||
done
|
done
|
||||||
unset _lth_cn
|
|
||||||
|
|
||||||
# ERE: safe colour reference $Name or ${Name}
|
|
||||||
local _lth_ref_re='^\$\{?('"$_lth_color_re"')\}?$'
|
|
||||||
|
|
||||||
# ERE: raw ANSI escape literal \e[...m or \033[...m
|
|
||||||
local _lth_ansi_re='^(\\e|\\033)\[[0-9;]*m$'
|
|
||||||
|
|
||||||
# ---- Line parser ---------------------------------------------------------
|
# ---- Line parser ---------------------------------------------------------
|
||||||
local _lth_line _lth_key _lth_value _lth_varname _lth_lineno=0
|
local _lth_line _lth_key _lth_value _lth_lineno=0
|
||||||
|
|
||||||
while IFS= read -r _lth_line || [[ -n "$_lth_line" ]]; do
|
while IFS= read -r _lth_line || [[ -n "$_lth_line" ]]; do
|
||||||
((_lth_lineno++))
|
_lth_lineno=$(( _lth_lineno + 1 ))
|
||||||
_lth_line="${_lth_line%$'\r'}" # strip CR
|
_lth_line="${_lth_line%$'\r'}" # strip CR
|
||||||
_lth_line="${_lth_line#"${_lth_line%%[![:space:]]*}"}" # ltrim
|
_lth_line="${_lth_line#"${_lth_line%%[![:space:]]*}"}" # ltrim
|
||||||
_lth_line="${_lth_line%"${_lth_line##*[![:space:]]}"}" # rtrim
|
_lth_line="${_lth_line%"${_lth_line##*[![:space:]]}"}" # rtrim
|
||||||
[[ -z "$_lth_line" || "$_lth_line" == '#'* ]] && continue # blank/comment
|
[[ -z "$_lth_line" || "$_lth_line" == '#'* ]] && continue # blank/comment
|
||||||
[[ "$_lth_line" == 'export '* ]] && _lth_line="${_lth_line#export }" # strip prefix
|
[[ "$_lth_line" == 'export '* ]] && _lth_line="${_lth_line#export }" # strip prefix
|
||||||
|
|
||||||
if [[ "$_lth_line" != *=* ]]; then
|
if [[ "$_lth_line" != *=* ]]; then
|
||||||
@@ -149,25 +120,16 @@ load_theme()
|
|||||||
_lth_value="${_lth_value%%\'*}"
|
_lth_value="${_lth_value%%\'*}"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ "$_lth_value" =~ $_lth_ref_re ]]; then
|
if _prompt_resolve_color "$_lth_value"; then
|
||||||
# Safe colour variable reference — resolve via indirection
|
export "$_lth_key"="$_PROMPT_RESOLVED"
|
||||||
_lth_varname="${_lth_value#\$}"
|
|
||||||
_lth_varname="${_lth_varname#\{}"
|
|
||||||
_lth_varname="${_lth_varname%\}}"
|
|
||||||
export "$_lth_key"="${!_lth_varname}"
|
|
||||||
elif [[ "$_lth_value" =~ $_lth_ansi_re ]]; then
|
|
||||||
# Raw ANSI escape literal — accept as-is
|
|
||||||
export "$_lth_key"="$_lth_value"
|
|
||||||
else
|
else
|
||||||
printf "[ Warning ] load_theme: %s:%d: invalid value for '%s', ignoring.\n" \
|
printf "[ Warning ] load_theme: %s:%d: invalid value for '%s', ignoring.\n" \
|
||||||
"$theme_file" "$_lth_lineno" "$_lth_key" >&2
|
"$theme_file" "$_lth_lineno" "$_lth_key" >&2
|
||||||
fi
|
fi
|
||||||
done < "$theme_file"
|
done < "$theme_file"
|
||||||
}
|
}
|
||||||
# Not exported, it remains private
|
# 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.
|
# Dynamically switch the prompt theme for the current shell session.
|
||||||
# Calls load_theme to apply the new colour values immediately, then updates
|
# Calls load_theme to apply the new colour values immediately, then updates
|
||||||
@@ -178,27 +140,86 @@ load_theme()
|
|||||||
set_theme()
|
set_theme()
|
||||||
{
|
{
|
||||||
local theme_dir="${PROMPT_THEME_DIR:-${MYPATH}/profile.d/themes}"
|
local theme_dir="${PROMPT_THEME_DIR:-${MYPATH}/profile.d/themes}"
|
||||||
|
local preview=0
|
||||||
|
local save=0
|
||||||
|
local list_only=0
|
||||||
|
local theme_name=""
|
||||||
|
|
||||||
# -- help mode -----------------------------------------------------------
|
while [[ $# -gt 0 ]]; do
|
||||||
if [[ "$1" == "-h" || "$1" == "--help" ]]; then
|
case "$1" in
|
||||||
printf "set_theme: Switch the prompt colour theme for the current shell session.\n\n"
|
-h|--help)
|
||||||
printf "Usage: set_theme [options] [theme]\n\n"
|
printf "set_theme: Switch the prompt colour theme for the current shell session.\n\n"
|
||||||
printf "Options:\n"
|
printf "Usage: set_theme [options] [theme]\n\n"
|
||||||
printf " -h, --help Display this help screen\n"
|
printf "Options:\n"
|
||||||
printf " -l, --list List available themes (default when no argument is given)\n\n"
|
printf "\t-h, --help\tDisplay this help screen\n"
|
||||||
printf "Arguments:\n"
|
printf "\t-l, --list\tList available themes (default when no argument is given)\n"
|
||||||
printf " theme Bare theme name (e.g. 'dark') or an explicit path to a .theme file.\n"
|
printf "\t-p, --preview\tPreview a theme without applying it\n"
|
||||||
printf " Themes are searched in: %s\n" "$theme_dir"
|
printf "\t-S, --save\tSave theme to configuration\n\n"
|
||||||
printf " Override with PROMPT_THEME_DIR in profile.conf [prompt].\n\n"
|
printf "Arguments:\n"
|
||||||
printf "Examples:\n"
|
printf "\ttheme \tBare theme name (e.g. 'dark') or an explicit path to a .theme file.\n"
|
||||||
printf " set_theme — list available themes\n"
|
printf "\t \tThemes are searched in: %s\n" "$theme_dir"
|
||||||
printf " set_theme dark — apply the dark theme\n"
|
printf "\t \tOverride with PROMPT_THEME_DIR in profile.conf [prompt].\n\n"
|
||||||
printf " set_theme ~/my.theme — apply a theme by path\n"
|
printf "Examples:\n"
|
||||||
return 0
|
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
|
fi
|
||||||
|
|
||||||
# -- list mode -----------------------------------------------------------
|
# -- list mode -----------------------------------------------------------
|
||||||
if [[ $# -eq 0 || "$1" == "-l" || "$1" == "--list" ]]; then
|
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"
|
printf "Available themes in %s:\n" "$theme_dir"
|
||||||
local f name
|
local f name
|
||||||
for f in "$theme_dir"/*.theme; do
|
for f in "$theme_dir"/*.theme; do
|
||||||
@@ -214,45 +235,101 @@ set_theme()
|
|||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# -- apply mode ----------------------------------------------------------
|
if (( preview )) && [[ -z "$theme_name" ]]; then
|
||||||
local theme_name="$1"
|
disp E "--preview requires a theme argument."
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
# Reset colours to defaults before loading the new theme
|
if (( preview && save )); then
|
||||||
set_colors
|
disp E "--preview and --save cannot be used together."
|
||||||
load_theme "$theme_name" || return 1
|
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"
|
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."
|
disp I "Prompt theme set to $theme_name."
|
||||||
}
|
}
|
||||||
export -f set_theme
|
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.
|
||||||
# timer_* functions : internal timing function for prompt
|
|
||||||
# Usage: timer_now
|
|
||||||
# This function returns the current time in nanoseconds since the epoch. It
|
|
||||||
# first tries to use the %N format specifier for nanoseconds, but if that is
|
|
||||||
# not supported (e.g., on older systems), it falls back to seconds.
|
|
||||||
function timer_now
|
|
||||||
{
|
|
||||||
date +%s%N 2>/dev/null || date +%s
|
|
||||||
}
|
|
||||||
|
|
||||||
# Usage: timer_start
|
# Usage: timer_start
|
||||||
# This function initializes the timer_start variable with the current time in
|
# Record the start time of the command about to run, unless already recorded
|
||||||
# nanoseconds. It is used to measure the elapsed time for the prompt.
|
# (the DEBUG trap fires for every simple command of a pipeline or function: only
|
||||||
function timer_start
|
# the first one counts).
|
||||||
|
timer_start()
|
||||||
{
|
{
|
||||||
timer_start=${timer_start:-$(timer_now)}
|
if [[ -z "${_timer_start_us:-}" ]]; then
|
||||||
|
_timer_now
|
||||||
|
# shellcheck disable=SC2154 # set by _timer_now (helpers/prompt.sh)
|
||||||
|
_timer_start_us="$_timer_us"
|
||||||
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
# Usage: timer_stop
|
# Usage: timer_stop
|
||||||
# This function calculates the elapsed time since timer_start and formats it
|
# Compute the time elapsed since timer_start and format it into timer_show with
|
||||||
# into a human-readable string with appropriate units (us, ms, s, m, h
|
# an appropriate unit (us, ms, s, m, h).
|
||||||
function timer_stop
|
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 us=$((delta_us % 1000))
|
||||||
local ms=$(((delta_us / 1000) % 1000))
|
local ms=$(((delta_us / 1000) % 1000))
|
||||||
local s=$(((delta_us / 1000000) % 60))
|
local s=$(((delta_us / 1000000) % 60))
|
||||||
@@ -266,7 +343,7 @@ function timer_stop
|
|||||||
elif ((s >= 10)); then
|
elif ((s >= 10)); then
|
||||||
timer_show=${s}.$((ms / 100))s
|
timer_show=${s}.$((ms / 100))s
|
||||||
elif ((s > 0)); then
|
elif ((s > 0)); then
|
||||||
timer_show=${s}.$(printf %03d $ms)s
|
printf -v timer_show '%d.%03ds' "$s" "$ms"
|
||||||
elif ((ms >= 100)); then
|
elif ((ms >= 100)); then
|
||||||
timer_show=${ms}ms
|
timer_show=${ms}ms
|
||||||
elif ((ms > 0)); then
|
elif ((ms > 0)); then
|
||||||
@@ -274,9 +351,16 @@ function timer_stop
|
|||||||
else
|
else
|
||||||
timer_show=${us}us
|
timer_show=${us}us
|
||||||
fi
|
fi
|
||||||
unset timer_start
|
unset _timer_start_us
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
# 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.
|
||||||
|
|
||||||
# ------------------------------------------------------------------------------
|
# ------------------------------------------------------------------------------
|
||||||
# Function triggered internally by bash : defining prompt
|
# Function triggered internally by bash : defining prompt
|
||||||
# Usage: set_prompt
|
# Usage: set_prompt
|
||||||
@@ -285,10 +369,23 @@ function timer_stop
|
|||||||
# command, the elapsed time of the last command, and the current user and host.
|
# command, the elapsed time of the last command, and the current user and host.
|
||||||
set_prompt()
|
set_prompt()
|
||||||
{
|
{
|
||||||
local Last_Command=$? # Must come first!
|
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 FancyX='\342\234\227'
|
||||||
local Checkmark='\342\234\223'
|
local Checkmark='\342\234\223'
|
||||||
|
|
||||||
|
# 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
|
# Resolve theme/config colours with hardcoded fallbacks
|
||||||
local _time_fg="${PROMPT_COLOR_TIME_FG:-$Blue}"
|
local _time_fg="${PROMPT_COLOR_TIME_FG:-$Blue}"
|
||||||
local _time_bg="${PROMPT_COLOR_TIME_BG:-$On_White}"
|
local _time_bg="${PROMPT_COLOR_TIME_BG:-$On_White}"
|
||||||
@@ -301,6 +398,7 @@ set_prompt()
|
|||||||
local _root_fg="${PROMPT_COLOR_ROOT_FG:-$Red}"
|
local _root_fg="${PROMPT_COLOR_ROOT_FG:-$Red}"
|
||||||
local _user_fg="${PROMPT_COLOR_USER_FG:-$BGreen}"
|
local _user_fg="${PROMPT_COLOR_USER_FG:-$BGreen}"
|
||||||
local _dir_fg="${PROMPT_COLOR_DIR_FG:-$ICyan}"
|
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
|
# Begin with time (cursor-save is non-printing; all ANSI sequences wrapped
|
||||||
# in \[...\] so bash does not count them toward the visible line width).
|
# in \[...\] so bash does not count them toward the visible line width).
|
||||||
@@ -310,33 +408,40 @@ set_prompt()
|
|||||||
|
|
||||||
# Add exit status of the last command.
|
# Add exit status of the last command.
|
||||||
# If it was successful, print a green check mark. Otherwise, print a red X.
|
# If it was successful, print a green check mark. Otherwise, print a red X.
|
||||||
|
timer_stop
|
||||||
if [[ $Last_Command == 0 ]]; then
|
if [[ $Last_Command == 0 ]]; then
|
||||||
PS1+="\[${_ok_fg}${_bar_bg}\] [ $Last_Command "
|
PS1+="\[${_ok_fg}${_bar_bg}\] [ $Last_Command "
|
||||||
PS1+="\[${_ok_mark}${_bar_bg}\]${Checkmark} "
|
PS1+="\[${_ok_mark}${_bar_bg}\]${Checkmark} "
|
||||||
# Add the elapsed time, then close the status section and return to bar bg.
|
# Add the elapsed time, then close the status section and return to bar bg.
|
||||||
timer_stop
|
|
||||||
PS1+="($timer_show)\[${_ok_fg}${_bar_bg}\] ] "
|
PS1+="($timer_show)\[${_ok_fg}${_bar_bg}\] ] "
|
||||||
else
|
else
|
||||||
PS1+="\[${_err_fg}${_err_bg}\] [ $Last_Command "
|
PS1+="\[${_err_fg}${_err_bg}\] [ $Last_Command "
|
||||||
PS1+="\[${_err_mark}${_err_bg}\]${FancyX} "
|
PS1+="\[${_err_mark}${_err_bg}\]${FancyX} "
|
||||||
timer_stop
|
|
||||||
PS1+="($timer_show)\[${_err_fg}${_err_bg}\] ] "
|
PS1+="($timer_show)\[${_err_fg}${_err_bg}\] ] "
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|
||||||
# If root, print the host in root colour. Otherwise use user colour.
|
# If root, print the host in root colour. Otherwise use user colour.
|
||||||
if [[ $EUID -eq 0 ]]; then
|
if [[ $EUID -eq 0 ]]; then
|
||||||
PS1+="\[${_root_fg}${_bar_bg}\] \\u\[${_user_fg}${_bar_bg}\]@\\h"
|
PS1+="\[${_root_fg}${_bar_bg}\] \\u\[${_user_fg}${_bar_bg}\]@\\h"
|
||||||
else
|
else
|
||||||
PS1+="\[${_user_fg}${_bar_bg}\] \\u@\\h"
|
PS1+="\[${_user_fg}${_bar_bg}\] \\u@\\h"
|
||||||
fi
|
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"
|
PS1+="\[\e[K\e[u\]\[$RESETCOL\]\n"
|
||||||
# Print the working directory and prompt marker, then reset colour.
|
# Print the working directory and prompt marker, then reset colour.
|
||||||
PS1+="\[${_dir_fg}\]\\w \\\$\[$RESETCOL\] "
|
PS1+="\[${_dir_fg}\]\\w \\\$\[$RESETCOL\] "
|
||||||
}
|
}
|
||||||
# ------------------------------------------------------------------------------
|
|
||||||
|
|
||||||
|
|
||||||
# ------------------------------------------------------------------------------
|
# ------------------------------------------------------------------------------
|
||||||
# Theme and configuration loading.
|
# Theme and configuration loading.
|
||||||
# Precedence (lowest → highest):
|
# Precedence (lowest → highest):
|
||||||
@@ -344,18 +449,17 @@ set_prompt()
|
|||||||
# 2. Theme file (PROMPT_THEME key from [prompt] section)
|
# 2. Theme file (PROMPT_THEME key from [prompt] section)
|
||||||
# 3. Individual PROMPT_COLOR_* overrides in [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
|
# CONF_prompt is already populated by parse_conf (run in profile.sh before
|
||||||
# modules are sourced). We extract PROMPT_THEME and PROMPT_THEME_DIR from the
|
# modules are sourced), which is where PROMPT_THEME and PROMPT_THEME_DIR are
|
||||||
# raw associative array now so load_theme can run before load_conf "prompt"
|
# read from.
|
||||||
# exports remaining keys. That way any PROMPT_COLOR_* value set explicitly in
|
|
||||||
# [prompt] wins over the same variable set by the theme file.
|
|
||||||
_pt_theme="${CONF_prompt[PROMPT_THEME]:-}"
|
_pt_theme="${CONF_prompt[PROMPT_THEME]:-}"
|
||||||
_pt_dir="${CONF_prompt[PROMPT_THEME_DIR]:-}"
|
_pt_dir="${CONF_prompt[PROMPT_THEME_DIR]:-}"
|
||||||
[[ -n "$_pt_theme" ]] && load_theme "$_pt_theme" ${_pt_dir:+"$_pt_dir"}
|
[[ -n "$_pt_theme" ]] && load_theme "$_pt_theme" ${_pt_dir:+"$_pt_dir"}
|
||||||
unset _pt_theme _pt_dir
|
unset _pt_theme _pt_dir
|
||||||
|
_prompt_apply_overrides
|
||||||
load_conf "prompt"
|
|
||||||
# ------------------------------------------------------------------------------
|
# ------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
# EOF
|
# EOF
|
||||||
|
|||||||
+45
-18
@@ -34,6 +34,13 @@
|
|||||||
# * OF SUCH DAMAGE.
|
# * 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
|
# genpwd : generate a password with different criteria
|
||||||
# Usage: genpwd [options] [--extracars=<cars>] [--length=<n>] [nb_passwd]
|
# Usage: genpwd [options] [--extracars=<cars>] [--length=<n>] [nb_passwd]
|
||||||
@@ -66,8 +73,14 @@ genpwd()
|
|||||||
help,nosymbols,nonumbers,noup,nolow,extracars:,length:,occurences:,occurrences: \
|
help,nosymbols,nonumbers,noup,nolow,extracars:,length:,occurences:,occurrences: \
|
||||||
-n 'genpwd' -- "$@")
|
-n 'genpwd' -- "$@")
|
||||||
# shellcheck disable=SC2181 # getopt return code is checked immediately after
|
# shellcheck disable=SC2181 # getopt return code is checked immediately after
|
||||||
if [[ $? -ne 0 ]]; then return 1; fi
|
if [[ $? -ne 0 ]]; then
|
||||||
eval set -- "$PARSED"
|
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
|
while true; do
|
||||||
case "$1" in
|
case "$1" in
|
||||||
@@ -146,6 +159,21 @@ genpwd()
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
local carset=""
|
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 unique_carset=""
|
||||||
local ch=""
|
local ch=""
|
||||||
local i=0
|
local i=0
|
||||||
@@ -206,7 +234,9 @@ genpwd()
|
|||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
disp I "Generating $nbpwd password(s), please wait..."
|
# 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
|
for (( n=1; n<=nbpwd; n++ )); do
|
||||||
local -a password_chars=()
|
local -a password_chars=()
|
||||||
local -A char_count=()
|
local -A char_count=()
|
||||||
@@ -220,7 +250,8 @@ genpwd()
|
|||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
idx=$(( RANDOM % ${#set} ))
|
_pwd_rand "${#set}" || return 1
|
||||||
|
idx=$_PWD_RAND
|
||||||
char=${set:idx:1}
|
char=${set:idx:1}
|
||||||
count=${char_count["$char"]:-0}
|
count=${char_count["$char"]:-0}
|
||||||
|
|
||||||
@@ -230,7 +261,7 @@ genpwd()
|
|||||||
break
|
break
|
||||||
fi
|
fi
|
||||||
|
|
||||||
((attempts++))
|
attempts=$(( attempts + 1 ))
|
||||||
done
|
done
|
||||||
done
|
done
|
||||||
|
|
||||||
@@ -242,7 +273,8 @@ genpwd()
|
|||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
idx=$(( RANDOM % ${#carset} ))
|
_pwd_rand "${#carset}" || return 1
|
||||||
|
idx=$_PWD_RAND
|
||||||
char=${carset:idx:1}
|
char=${carset:idx:1}
|
||||||
count=${char_count["$char"]:-0}
|
count=${char_count["$char"]:-0}
|
||||||
|
|
||||||
@@ -252,12 +284,13 @@ genpwd()
|
|||||||
break
|
break
|
||||||
fi
|
fi
|
||||||
|
|
||||||
((attempts++))
|
attempts=$(( attempts + 1 ))
|
||||||
done
|
done
|
||||||
done
|
done
|
||||||
|
|
||||||
for (( i=${#password_chars[@]} - 1; i>0; i-- )); do
|
for (( i=${#password_chars[@]} - 1; i>0; i-- )); do
|
||||||
idx=$(( RANDOM % (i + 1) ))
|
_pwd_rand "$(( i + 1 ))" || return 1
|
||||||
|
idx=$_PWD_RAND
|
||||||
char=${password_chars[i]}
|
char=${password_chars[i]}
|
||||||
password_chars[i]=${password_chars[idx]}
|
password_chars[i]=${password_chars[idx]}
|
||||||
password_chars[idx]=$char
|
password_chars[idx]=$char
|
||||||
@@ -268,9 +301,6 @@ genpwd()
|
|||||||
done
|
done
|
||||||
}
|
}
|
||||||
export -f genpwd
|
export -f genpwd
|
||||||
# ------------------------------------------------------------------------------
|
|
||||||
|
|
||||||
|
|
||||||
# ------------------------------------------------------------------------------
|
# ------------------------------------------------------------------------------
|
||||||
# pwdscore : score a password quality from 1 to 100
|
# pwdscore : score a password quality from 1 to 100
|
||||||
# Usage: pwdscore [options] <password>
|
# Usage: pwdscore [options] <password>
|
||||||
@@ -308,7 +338,7 @@ pwdscore()
|
|||||||
break
|
break
|
||||||
;;
|
;;
|
||||||
-*)
|
-*)
|
||||||
disp E "Invalid option '$1'. Use \"pwdscore --help\" to display usage."
|
disp E "Invalid option '$1'. Use \"pwdscore --help\" to display usage (a password starting with '-' must follow '--')."
|
||||||
return 1
|
return 1
|
||||||
;;
|
;;
|
||||||
*)
|
*)
|
||||||
@@ -369,17 +399,17 @@ pwdscore()
|
|||||||
score=$(( length * 2 ))
|
score=$(( length * 2 ))
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ $password =~ [a-z] ]]; then
|
if [[ $password =~ [[:lower:]] ]]; then
|
||||||
has_lower=1
|
has_lower=1
|
||||||
pool_size=$(( pool_size + 26 ))
|
pool_size=$(( pool_size + 26 ))
|
||||||
score=$(( score + 12 ))
|
score=$(( score + 12 ))
|
||||||
fi
|
fi
|
||||||
if [[ $password =~ [A-Z] ]]; then
|
if [[ $password =~ [[:upper:]] ]]; then
|
||||||
has_upper=1
|
has_upper=1
|
||||||
pool_size=$(( pool_size + 26 ))
|
pool_size=$(( pool_size + 26 ))
|
||||||
score=$(( score + 12 ))
|
score=$(( score + 12 ))
|
||||||
fi
|
fi
|
||||||
if [[ $password =~ [0-9] ]]; then
|
if [[ $password =~ [[:digit:]] ]]; then
|
||||||
has_digit=1
|
has_digit=1
|
||||||
pool_size=$(( pool_size + 10 ))
|
pool_size=$(( pool_size + 10 ))
|
||||||
score=$(( score + 12 ))
|
score=$(( score + 12 ))
|
||||||
@@ -477,7 +507,4 @@ pwdscore()
|
|||||||
export -f pwdscore
|
export -f pwdscore
|
||||||
# ------------------------------------------------------------------------------
|
# ------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
load_conf "pwd"
|
|
||||||
|
|
||||||
# EOF
|
# EOF
|
||||||
|
|||||||
+188
-251
@@ -34,261 +34,37 @@
|
|||||||
# * OF SUCH DAMAGE.
|
# * OF SUCH DAMAGE.
|
||||||
# ------------------------------------------------------------------------------
|
# ------------------------------------------------------------------------------
|
||||||
|
|
||||||
# ------------------------------------------------------------------------------
|
load_conf "rain"
|
||||||
# Generic rain-like engine and presets
|
|
||||||
|
|
||||||
_rain_build_colors()
|
# Common helpers, then the ones private to this module (helpers/rain.sh)
|
||||||
{
|
# shellcheck source=../helpers/common.sh
|
||||||
local base_color="$1"
|
declare -F _profile_load_helpers >/dev/null || . "$(dirname -- "${BASH_SOURCE[0]}")/../helpers/common.sh"
|
||||||
RAIN_ENGINE_COLORS=()
|
_profile_load_helpers "${BASH_SOURCE[0]}"
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
_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
|
|
||||||
}
|
|
||||||
|
|
||||||
_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
|
|
||||||
|
|
||||||
if awk -v s="$raw_speed" 'BEGIN { exit !(s < 1) }'; then
|
|
||||||
printf "%s" "$raw_speed"
|
|
||||||
else
|
|
||||||
awk -v s="$raw_speed" 'BEGIN { printf "%.3f", s / 100 }'
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
_rain_engine()
|
|
||||||
{
|
|
||||||
local step_duration="$1"
|
|
||||||
local base_color="$2"
|
|
||||||
local mode="$3"
|
|
||||||
local charset="$4"
|
|
||||||
|
|
||||||
command -v tput >/dev/null 2>&1 || {
|
|
||||||
disp E "The program 'tput' is required but not installed."
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
_rain_build_colors "$base_color"
|
|
||||||
_rain_build_chars "$mode" "$charset" || return 1
|
|
||||||
|
|
||||||
local rain_colors=("${RAIN_ENGINE_COLORS[@]}")
|
|
||||||
local rain_chars=("${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"
|
|
||||||
|
|
||||||
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=$(awk -v s="$step_duration" 'BEGIN { printf "%.3f", s * 1.15 }')
|
|
||||||
elif ((term_area > 5000)); then
|
|
||||||
((max_rain_width = term_area / 2))
|
|
||||||
frame_sleep=$(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
|
|
||||||
}
|
|
||||||
|
|
||||||
do_exit()
|
|
||||||
{
|
|
||||||
exit_st=1
|
|
||||||
}
|
|
||||||
|
|
||||||
do_render()
|
|
||||||
{
|
|
||||||
local idx=0 y=0 drop_color="" current_char="" render_color=""
|
|
||||||
|
|
||||||
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
|
|
||||||
printf "\e[%d;%dH " "$y" "$X"
|
|
||||||
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
|
|
||||||
rains=("${rains[@]:0:idx}" "${rains[@]:idx+num_rain_metadata:num_rains*num_rain_metadata}")
|
|
||||||
((num_rains--))
|
|
||||||
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
|
|
||||||
printf "\e[%d;%dH%b%s" "$y" "$X" "$render_color" "$current_char"
|
|
||||||
done
|
|
||||||
done
|
|
||||||
}
|
|
||||||
|
|
||||||
trap do_exit TERM INT
|
|
||||||
trap sigwinch WINCH
|
|
||||||
stty -echo
|
|
||||||
printf "\e[?25l"
|
|
||||||
printf "\e[2J"
|
|
||||||
|
|
||||||
local rains=()
|
|
||||||
local num_rains=0
|
|
||||||
local ch=""
|
|
||||||
|
|
||||||
sigwinch
|
|
||||||
while ((exit_st <= 0)); do
|
|
||||||
read -r -n 1 -t "$frame_sleep" ch
|
|
||||||
case "$ch" in
|
|
||||||
q|Q)
|
|
||||||
do_exit
|
|
||||||
;;
|
|
||||||
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]}"
|
|
||||||
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
|
|
||||||
|
|
||||||
do_render
|
|
||||||
done
|
|
||||||
|
|
||||||
printf "\e[%d;1H\e[0K" "$term_height"
|
|
||||||
printf "\e[?25h"
|
|
||||||
stty echo
|
|
||||||
trap - TERM INT
|
|
||||||
trap - WINCH
|
|
||||||
}
|
|
||||||
|
|
||||||
# ------------------------------------------------------------------------------
|
# ------------------------------------------------------------------------------
|
||||||
# Let the rain fall (current style)
|
# Let the rain fall (current style)
|
||||||
# Usage: rain [OPTIONS]
|
# Usage: rain [OPTIONS]
|
||||||
rain()
|
rain()
|
||||||
{
|
{
|
||||||
|
local _rain_show_usage
|
||||||
_rain_show_usage()
|
_rain_show_usage()
|
||||||
{
|
{
|
||||||
printf "Usage: rain [OPTIONS]\n"
|
printf "Usage: rain [OPTIONS]\n"
|
||||||
printf "Options:\n"
|
printf "Options:\n"
|
||||||
printf "\t-s, --speed NUM Set speed value (default: 5 => 0.050s).\n"
|
printf "\t-s, --speed NUM\tSet speed value (default: 5 => 0.050s).\n"
|
||||||
printf "\t Values >=1 use a /100 scale (5 => 0.05s).\n"
|
printf "\t\t\t\tValues >=1 use a /100 scale (5 => 0.05s).\n"
|
||||||
printf "\t Values <1 are interpreted as raw seconds.\n"
|
printf "\t\t\t\tValues <1 are interpreted as raw seconds.\n"
|
||||||
printf "\t-c, --color COLOR Set the color theme (default: white).\n"
|
printf "\t-d, --density NUM\tMaximum number of simultaneous falling elements.\n"
|
||||||
printf "\t-h, --help Display this help message and exit.\n\n"
|
printf "\t-c, --color COLOR\tSet the color theme (default: white).\n"
|
||||||
printf "Available Colors:\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[32mgreen\e[0m\t: Matrix-like green shades\n"
|
||||||
printf "\t\e[34mblue\e[0m\t: Deep ocean blue gradients\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[31mred\e[0m\t: Crimson/Blood rain\n"
|
||||||
printf "\t\e[33myellow\e[0m\t: Amber and gold tones\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 "\t\e[36mcyan\e[0m\t: Electric cyan/turquoise\n"
|
||||||
printf "\twhite\t: Greyscale and white (original style)\n\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"
|
printf "Example: rain --color green --speed 3\n"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -296,19 +72,24 @@ rain()
|
|||||||
local step_duration
|
local step_duration
|
||||||
step_duration=$(_rain_normalize_speed "$_raw_speed") || step_duration=0.050
|
step_duration=$(_rain_normalize_speed "$_raw_speed") || step_duration=0.050
|
||||||
local base_color="${RAIN_DEFAULT_COLOR:-white}"
|
local base_color="${RAIN_DEFAULT_COLOR:-white}"
|
||||||
|
local density_override="${RAIN_DEFAULT_DENSITY:-}"
|
||||||
|
|
||||||
|
if [[ -n "$density_override" ]]; then
|
||||||
|
density_override=$(_rain_normalize_density "$density_override") || density_override=""
|
||||||
|
fi
|
||||||
|
|
||||||
while [[ "$#" -gt 0 ]]; do
|
while [[ "$#" -gt 0 ]]; do
|
||||||
case $1 in
|
case $1 in
|
||||||
-s|--speed)
|
-s|--speed)
|
||||||
if [[ -n "$2" && ! "$2" =~ ^- ]]; then
|
if [[ -n "$2" && ! "$2" =~ ^- ]]; then
|
||||||
step_duration=$(_rain_normalize_speed "$2") || {
|
step_duration=$(_rain_normalize_speed "$2") || {
|
||||||
disp E "--speed requires a numeric value."
|
disp E "--speed requires a numeric value greater than 0."
|
||||||
_rain_show_usage
|
_rain_show_usage
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
shift
|
shift
|
||||||
else
|
else
|
||||||
disp E "--speed requires a numeric value."
|
disp E "--speed requires a numeric value greater than 0."
|
||||||
_rain_show_usage
|
_rain_show_usage
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
@@ -323,6 +104,20 @@ rain()
|
|||||||
return 1
|
return 1
|
||||||
fi
|
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)
|
-h|--help)
|
||||||
_rain_show_usage
|
_rain_show_usage
|
||||||
return 0
|
return 0
|
||||||
@@ -340,7 +135,7 @@ rain()
|
|||||||
shift
|
shift
|
||||||
done
|
done
|
||||||
|
|
||||||
_rain_engine "$step_duration" "$base_color" "rain" ""
|
_rain_engine "$step_duration" "$base_color" "rain" "" "$density_override"
|
||||||
}
|
}
|
||||||
export -f rain
|
export -f rain
|
||||||
|
|
||||||
@@ -349,16 +144,19 @@ export -f rain
|
|||||||
# Usage: matrix [OPTIONS]
|
# Usage: matrix [OPTIONS]
|
||||||
matrix()
|
matrix()
|
||||||
{
|
{
|
||||||
|
local _matrix_show_usage
|
||||||
_matrix_show_usage()
|
_matrix_show_usage()
|
||||||
{
|
{
|
||||||
printf "Usage: matrix [OPTIONS]\n"
|
printf "Usage: matrix [OPTIONS]\n"
|
||||||
printf "Options:\n"
|
printf "Options:\n"
|
||||||
printf "\t-s, --speed NUM Set speed value (default: 3.5 => 0.035s).\n"
|
printf "\t-s, --speed NUM\tSet speed value (default: 3.5 => 0.035s).\n"
|
||||||
printf "\t Values >=1 use a /100 scale (3.5 => 0.035s).\n"
|
printf "\t\t\t\tValues >=1 use a /100 scale (3.5 => 0.035s).\n"
|
||||||
printf "\t Values <1 are interpreted as raw seconds.\n"
|
printf "\t\t\t\tValues <1 are interpreted as raw seconds.\n"
|
||||||
printf "\t-c, --color COLOR Set color theme (default: green).\n"
|
printf "\t-d, --density NUM\tMaximum number of simultaneous falling elements.\n"
|
||||||
printf "\t-C, --charset SET Character set: binary, kana, ascii (default: binary).\n"
|
printf "\t-c, --color COLOR\tSet color theme (default: green).\n"
|
||||||
printf "\t-h, --help Display this help message and exit.\n\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"
|
printf "Example: matrix -C kana -c green --speed 2\n"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -366,20 +164,27 @@ matrix()
|
|||||||
local step_duration
|
local step_duration
|
||||||
step_duration=$(_rain_normalize_speed "$_raw_speed") || step_duration=0.035
|
step_duration=$(_rain_normalize_speed "$_raw_speed") || step_duration=0.035
|
||||||
local base_color="${MATRIX_DEFAULT_COLOR:-green}"
|
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}"
|
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
|
while [[ "$#" -gt 0 ]]; do
|
||||||
case $1 in
|
case $1 in
|
||||||
-s|--speed)
|
-s|--speed)
|
||||||
if [[ -n "$2" && ! "$2" =~ ^- ]]; then
|
if [[ -n "$2" && ! "$2" =~ ^- ]]; then
|
||||||
step_duration=$(_rain_normalize_speed "$2") || {
|
step_duration=$(_rain_normalize_speed "$2") || {
|
||||||
disp E "--speed requires a numeric value."
|
disp E "--speed requires a numeric value greater than 0."
|
||||||
_matrix_show_usage
|
_matrix_show_usage
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
shift
|
shift
|
||||||
else
|
else
|
||||||
disp E "--speed requires a numeric value."
|
disp E "--speed requires a numeric value greater than 0."
|
||||||
_matrix_show_usage
|
_matrix_show_usage
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
@@ -412,6 +217,20 @@ matrix()
|
|||||||
return 1
|
return 1
|
||||||
fi
|
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)
|
-h|--help)
|
||||||
_matrix_show_usage
|
_matrix_show_usage
|
||||||
return 0
|
return 0
|
||||||
@@ -429,12 +248,130 @@ matrix()
|
|||||||
shift
|
shift
|
||||||
done
|
done
|
||||||
|
|
||||||
_rain_engine "$step_duration" "$base_color" "matrix" "$charset"
|
_rain_engine "$step_duration" "$base_color" "matrix" "$charset" "$density_override"
|
||||||
}
|
}
|
||||||
export -f matrix
|
export -f matrix
|
||||||
|
|
||||||
# ------------------------------------------------------------------------------
|
# ------------------------------------------------------------------------------
|
||||||
|
# Full-screen rainbow screensaver
|
||||||
|
# Usage: rainbow [OPTIONS]
|
||||||
|
rainbow()
|
||||||
|
{
|
||||||
|
local PARSED
|
||||||
|
|
||||||
load_conf "rain"
|
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
|
# EOF
|
||||||
|
|||||||
+111
-31
@@ -34,6 +34,13 @@
|
|||||||
# * OF SUCH DAMAGE.
|
# * OF SUCH DAMAGE.
|
||||||
# ------------------------------------------------------------------------------
|
# ------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
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
|
# Remove host entries (name and IP) from ~/.ssh/known_hosts for the active user
|
||||||
# Usage: rmhost <hostname|ip> [hostname2|ip2 [...]]
|
# Usage: rmhost <hostname|ip> [hostname2|ip2 [...]]
|
||||||
@@ -45,17 +52,27 @@ rmhost()
|
|||||||
|
|
||||||
PARSED=$(getopt -o ha --long help,all-users -n 'rmhost' -- "$@")
|
PARSED=$(getopt -o ha --long help,all-users -n 'rmhost' -- "$@")
|
||||||
# shellcheck disable=SC2181 # getopt return code is checked immediately after
|
# shellcheck disable=SC2181 # getopt return code is checked immediately after
|
||||||
if [[ $? -ne 0 ]]; then return 1; fi
|
if [[ $? -ne 0 ]]; then
|
||||||
eval set -- "$PARSED"
|
disp E "Invalid options, use \"rmhost --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
|
while true; do
|
||||||
case "$1" in
|
case "$1" in
|
||||||
-h|--help)
|
-h|--help)
|
||||||
printf "rmhost: Remove host/IP from known_hosts files.\n\n"
|
printf "rmhost: Remove host/IP from known_hosts files.\n\n"
|
||||||
printf "Usage: rmhost [--all-users] <hostname|ip> [hostname2|ip2 ...]\n\n"
|
printf "Usage: rmhost [--all-users] <pattern|ip> [pattern2|ip2 ...]\n\n"
|
||||||
printf "Options:\n"
|
printf "Options:\n"
|
||||||
printf " -a, --all-users Remove entries from all local users when run as root\n"
|
printf " -a, --all-users Remove entries from all local users when run as root\n"
|
||||||
printf " -h, --help Display this help screen\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
|
return 0
|
||||||
;;
|
;;
|
||||||
-a|--all-users)
|
-a|--all-users)
|
||||||
@@ -83,14 +100,27 @@ rmhost()
|
|||||||
return 127
|
return 127
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# 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
|
if (( all_users )); then
|
||||||
[[ ${EUID:-$(id -u)} -eq 0 ]] || {
|
[[ ${EUID:-$(id -u)} -eq 0 ]] || {
|
||||||
disp E "Option --all-users is only available when run as root."
|
disp E "Option --all-users is only available when run as root."
|
||||||
return 1
|
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
|
while IFS=: read -r _ _ _ _ _ home _; do
|
||||||
[[ -n $home && -f $home/.ssh/known_hosts ]] || continue
|
[[ -n $home && -f $home/.ssh/known_hosts && -z ${_seen_homes[$home]+x} ]] || continue
|
||||||
|
_seen_homes[$home]=1
|
||||||
known_hosts_files+=("$home/.ssh/known_hosts")
|
known_hosts_files+=("$home/.ssh/known_hosts")
|
||||||
done < /etc/passwd
|
done < /etc/passwd
|
||||||
|
|
||||||
@@ -102,34 +132,81 @@ rmhost()
|
|||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
else
|
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")
|
known_hosts_files=("${HOME}/.ssh/known_hosts")
|
||||||
fi
|
fi
|
||||||
|
|
||||||
for target in "$@"; do
|
for target in "$@"; do
|
||||||
local hst="$target"
|
# Wildcard: expand the glob pattern against the unhashed entries of
|
||||||
local ip=""
|
# each file, and remove the matches from that file only. A "[host]:port"
|
||||||
local v4=1
|
# entry is not a pattern: its brackets are known_hosts notation.
|
||||||
local v6=1
|
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"
|
||||||
|
|
||||||
isipv4 "$hst" >/dev/null 2>&1; v4=$?
|
[[ ${#_matched[@]} -gt 0 ]] || continue
|
||||||
isipv6 "$hst" >/dev/null 2>&1; v6=$?
|
_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
|
||||||
|
|
||||||
if [[ $v4 -eq 0 || $v6 -eq 0 ]]; then
|
(( _found )) || disp W "No known_hosts entries match pattern '$target'."
|
||||||
ip="$hst"
|
continue
|
||||||
hst=""
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ -z ${ip:-} && -n ${hst:-} ]]; then
|
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
|
if command -v host >/dev/null 2>&1; then
|
||||||
ip=$(host "$hst" 2>/dev/null |
|
mapfile -t ips < <(host "$hst" 2>/dev/null |
|
||||||
awk '/has address|has IPv6 address/ {print $NF; exit}')
|
awk '/has address|has IPv6 address/ {print $NF}' | sort -u)
|
||||||
elif command -v getent >/dev/null 2>&1; then
|
elif command -v getent >/dev/null 2>&1; then
|
||||||
ip=$(getent ahosts "$hst" 2>/dev/null | awk 'NR == 1 {print $1; exit}')
|
# ahosts lists every address once per socket type
|
||||||
|
mapfile -t ips < <(getent ahosts "$hst" 2>/dev/null |
|
||||||
|
awk '{print $1}' | sort -u)
|
||||||
else
|
else
|
||||||
disp W "No resolver tool found; removing hostname only for '$hst'."
|
disp W "No resolver tool found; removing hostname only for '$hst'."
|
||||||
fi
|
fi
|
||||||
|
|
||||||
[[ -z ${ip:-} ]] && \
|
[[ ${#ips[@]} -eq 0 ]] && \
|
||||||
disp W "Could not resolve IP for '$hst'; removing hostname only."
|
disp W "Could not resolve IP for '$hst'; removing hostname only."
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -137,23 +214,20 @@ rmhost()
|
|||||||
for known_hosts_file in "${known_hosts_files[@]}"; do
|
for known_hosts_file in "${known_hosts_files[@]}"; do
|
||||||
if [[ -n ${hst:-} ]]; then
|
if [[ -n ${hst:-} ]]; then
|
||||||
disp I "Removing host $hst from $known_hosts_file..."
|
disp I "Removing host $hst from $known_hosts_file..."
|
||||||
if ! ssh-keygen -R "$hst" -f "$known_hosts_file" >/dev/null 2>&1; then
|
if ! _rmhost_remove "$hst" "$known_hosts_file"; then
|
||||||
disp W "No known_hosts entry found for '$hst' in '$known_hosts_file'."
|
disp W "No known_hosts entry found for '$hst' in '$known_hosts_file'."
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
if [[ -n ${ip:-} ]]; then
|
for ip in "${ips[@]}"; do
|
||||||
disp I "Removing IP $ip from $known_hosts_file..."
|
disp I "Removing IP $ip from $known_hosts_file..."
|
||||||
if ! ssh-keygen -R "$ip" -f "$known_hosts_file" >/dev/null 2>&1; then
|
if ! _rmhost_remove "$ip" "$known_hosts_file"; then
|
||||||
disp W "No known_hosts entry found for '$ip' in '$known_hosts_file'."
|
disp W "No known_hosts entry found for '$ip' in '$known_hosts_file'."
|
||||||
fi
|
fi
|
||||||
fi
|
done
|
||||||
done
|
done
|
||||||
done
|
done
|
||||||
}
|
}
|
||||||
export -f rmhost
|
export -f rmhost
|
||||||
# ------------------------------------------------------------------------------
|
|
||||||
|
|
||||||
|
|
||||||
# ------------------------------------------------------------------------------
|
# ------------------------------------------------------------------------------
|
||||||
# Login root via SSH on the given machine
|
# Login root via SSH on the given machine
|
||||||
# Usage: ssr <server [ssh options]>
|
# Usage: ssr <server [ssh options]>
|
||||||
@@ -187,9 +261,18 @@ ssr()
|
|||||||
local srv=$1
|
local srv=$1
|
||||||
shift
|
shift
|
||||||
|
|
||||||
# Build default options array from config, falling back to -Y (X11 forwarding)
|
# 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=()
|
local -a ssh_default_opts=()
|
||||||
if [[ -n "${SSH_DEFAULT_OPT:-}" ]]; then
|
if [[ -n "${SSH_DEFAULT_OPT+x}" ]]; then
|
||||||
read -ra ssh_default_opts <<< "${SSH_DEFAULT_OPT}"
|
read -ra ssh_default_opts <<< "${SSH_DEFAULT_OPT}"
|
||||||
else
|
else
|
||||||
ssh_default_opts=(-Y)
|
ssh_default_opts=(-Y)
|
||||||
@@ -201,7 +284,4 @@ ssr()
|
|||||||
export -f ssr
|
export -f ssr
|
||||||
# ------------------------------------------------------------------------------
|
# ------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
load_conf "ssh"
|
|
||||||
|
|
||||||
# EOF
|
# EOF
|
||||||
|
|||||||
@@ -40,3 +40,4 @@ PROMPT_COLOR_ERR_MARK="$IYellow" # golden X
|
|||||||
PROMPT_COLOR_ROOT_FG="$IRed" # red for root
|
PROMPT_COLOR_ROOT_FG="$IRed" # red for root
|
||||||
PROMPT_COLOR_USER_FG="$IBlue" # electric blue for user
|
PROMPT_COLOR_USER_FG="$IBlue" # electric blue for user
|
||||||
PROMPT_COLOR_DIR_FG="$ICyan" # teal path
|
PROMPT_COLOR_DIR_FG="$ICyan" # teal path
|
||||||
|
PROMPT_COLOR_CTX_FG="$IYellow" # context segment (git/conda)
|
||||||
|
|||||||
@@ -40,3 +40,4 @@ PROMPT_COLOR_ERR_MARK="$Yellow" # yellow X (warning intent)
|
|||||||
PROMPT_COLOR_ROOT_FG="$Red" # Adwaita red for root
|
PROMPT_COLOR_ROOT_FG="$Red" # Adwaita red for root
|
||||||
PROMPT_COLOR_USER_FG="$BBlue" # darker bold blue — readable on blue bar
|
PROMPT_COLOR_USER_FG="$BBlue" # darker bold blue — readable on blue bar
|
||||||
PROMPT_COLOR_DIR_FG="$IGreen" # Adwaita green for path
|
PROMPT_COLOR_DIR_FG="$IGreen" # Adwaita green for path
|
||||||
|
PROMPT_COLOR_CTX_FG="$BIWhite" # context segment (git/conda)
|
||||||
|
|||||||
@@ -30,3 +30,4 @@ PROMPT_COLOR_ERR_MARK="$BIYellow" # X mark colour on failure
|
|||||||
PROMPT_COLOR_ROOT_FG="$BIRed" # Username colour when root
|
PROMPT_COLOR_ROOT_FG="$BIRed" # Username colour when root
|
||||||
PROMPT_COLOR_USER_FG="$ICyan" # Username@host colour for normal users
|
PROMPT_COLOR_USER_FG="$ICyan" # Username@host colour for normal users
|
||||||
PROMPT_COLOR_DIR_FG="$IPurple" # Working directory colour
|
PROMPT_COLOR_DIR_FG="$IPurple" # Working directory colour
|
||||||
|
PROMPT_COLOR_CTX_FG="$BIYellow" # Context segment (git/conda)
|
||||||
|
|||||||
@@ -30,3 +30,4 @@ PROMPT_COLOR_ERR_MARK="$BYellow" # X mark colour on failure
|
|||||||
PROMPT_COLOR_ROOT_FG="$Red" # Username colour when root
|
PROMPT_COLOR_ROOT_FG="$Red" # Username colour when root
|
||||||
PROMPT_COLOR_USER_FG="$Green" # Username@host colour for normal users
|
PROMPT_COLOR_USER_FG="$Green" # Username@host colour for normal users
|
||||||
PROMPT_COLOR_DIR_FG="$ICyan" # Working directory colour
|
PROMPT_COLOR_DIR_FG="$ICyan" # Working directory colour
|
||||||
|
PROMPT_COLOR_CTX_FG="$BIYellow" # Context segment (git/conda)
|
||||||
|
|||||||
@@ -33,3 +33,4 @@ PROMPT_COLOR_ERR_MARK="$BYellow" # X mark on failure (BIYellow → BYellow, l
|
|||||||
PROMPT_COLOR_ROOT_FG="$Red" # Username when root (BIRed → Red)
|
PROMPT_COLOR_ROOT_FG="$Red" # Username when root (BIRed → Red)
|
||||||
PROMPT_COLOR_USER_FG="$Blue" # Username@host normal user (ICyan → Blue)
|
PROMPT_COLOR_USER_FG="$Blue" # Username@host normal user (ICyan → Blue)
|
||||||
PROMPT_COLOR_DIR_FG="$Purple" # Working directory (IPurple → Purple)
|
PROMPT_COLOR_DIR_FG="$Purple" # Working directory (IPurple → Purple)
|
||||||
|
PROMPT_COLOR_CTX_FG="$BBlack" # Context segment (git/conda)
|
||||||
|
|||||||
@@ -62,3 +62,4 @@ PROMPT_COLOR_ERR_MARK="$BBlack" # bold black X
|
|||||||
PROMPT_COLOR_ROOT_FG="$BIWhite" # bold bright white for root warning
|
PROMPT_COLOR_ROOT_FG="$BIWhite" # bold bright white for root warning
|
||||||
PROMPT_COLOR_USER_FG="$IWhite" # bright white for normal user
|
PROMPT_COLOR_USER_FG="$IWhite" # bright white for normal user
|
||||||
PROMPT_COLOR_DIR_FG="$White" # standard white for path
|
PROMPT_COLOR_DIR_FG="$White" # standard white for path
|
||||||
|
PROMPT_COLOR_CTX_FG="$BIWhite" # context segment (git/conda)
|
||||||
|
|||||||
@@ -43,3 +43,4 @@ PROMPT_COLOR_ERR_MARK="$IRed" # hot pink X
|
|||||||
PROMPT_COLOR_ROOT_FG="$IRed" # hot pink for root
|
PROMPT_COLOR_ROOT_FG="$IRed" # hot pink for root
|
||||||
PROMPT_COLOR_USER_FG="$IYellow" # orange-yellow for user
|
PROMPT_COLOR_USER_FG="$IYellow" # orange-yellow for user
|
||||||
PROMPT_COLOR_DIR_FG="$ICyan" # electric cyan for path
|
PROMPT_COLOR_DIR_FG="$ICyan" # electric cyan for path
|
||||||
|
PROMPT_COLOR_CTX_FG="$IYellow" # context segment (git/conda)
|
||||||
|
|||||||
@@ -40,3 +40,4 @@ PROMPT_COLOR_ERR_MARK="$IYellow" # yellow X
|
|||||||
PROMPT_COLOR_ROOT_FG="$IRed" # red for root
|
PROMPT_COLOR_ROOT_FG="$IRed" # red for root
|
||||||
PROMPT_COLOR_USER_FG="$BIPurple" # bold vivid purple for user
|
PROMPT_COLOR_USER_FG="$BIPurple" # bold vivid purple for user
|
||||||
PROMPT_COLOR_DIR_FG="$ICyan" # electric cyan path
|
PROMPT_COLOR_DIR_FG="$ICyan" # electric cyan path
|
||||||
|
PROMPT_COLOR_CTX_FG="$IYellow" # context segment (git/conda)
|
||||||
|
|||||||
@@ -125,3 +125,4 @@ 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_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_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_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)
|
||||||
|
|||||||
@@ -120,3 +120,4 @@ PROMPT_COLOR_ERR_MARK="\e[1;38;2;253;246;227m" # Base3 bold — bright warm mark
|
|||||||
PROMPT_COLOR_ROOT_FG="\e[38;2;220;50;47m" # Red — root warning
|
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_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_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)
|
||||||
|
|||||||
+133
-60
@@ -40,6 +40,18 @@ export BASE_URL="https://git.geoffray-levasseur.org/fatalerrors/profile"
|
|||||||
# UPDT_DEFAULT_BRANCH (if set in profile.conf) is already available.
|
# UPDT_DEFAULT_BRANCH (if set in profile.conf) is already available.
|
||||||
load_conf "updates"
|
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_DEFAULT_BRANCH="${UPDT_DEFAULT_BRANCH:-master}"
|
||||||
export UPDT_URL="$BASE_URL/raw/branch/$UPDT_DEFAULT_BRANCH"
|
export UPDT_URL="$BASE_URL/raw/branch/$UPDT_DEFAULT_BRANCH"
|
||||||
export ARCH_URL="$BASE_URL/archive/$UPDT_DEFAULT_BRANCH.tar.gz"
|
export ARCH_URL="$BASE_URL/archive/$UPDT_DEFAULT_BRANCH.tar.gz"
|
||||||
@@ -47,7 +59,10 @@ export ARCH_URL="$BASE_URL/archive/$UPDT_DEFAULT_BRANCH.tar.gz"
|
|||||||
# ------------------------------------------------------------------------------
|
# ------------------------------------------------------------------------------
|
||||||
# Check whether a newer profile version is available
|
# Check whether a newer profile version is available
|
||||||
# Usage: check_updates [-q]
|
# Usage: check_updates [-q]
|
||||||
# If -q is specified, the function will operate in quiet mode (internal use only)
|
# 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()
|
check_updates()
|
||||||
{
|
{
|
||||||
local quiet=0 result=5 PARSED
|
local quiet=0 result=5 PARSED
|
||||||
@@ -59,7 +74,10 @@ check_updates()
|
|||||||
disp E "Invalid options, use \"check_updates --help\" to display usage."
|
disp E "Invalid options, use \"check_updates --help\" to display usage."
|
||||||
return 2
|
return 2
|
||||||
fi
|
fi
|
||||||
eval set -- "$PARSED"
|
# Restore the getopt-normalised (shell-quoted) argument list
|
||||||
|
local -a _pargs=()
|
||||||
|
_getopt_split _pargs "$PARSED"
|
||||||
|
set -- "${_pargs[@]}"
|
||||||
|
|
||||||
while true; do
|
while true; do
|
||||||
case "$1" in
|
case "$1" in
|
||||||
@@ -85,42 +103,57 @@ check_updates()
|
|||||||
|
|
||||||
(( quiet != 1 )) && disp I "Checking for updates..."
|
(( quiet != 1 )) && disp I "Checking for updates..."
|
||||||
|
|
||||||
vfile=$(mktemp /tmp/profile_version.XXXXXX) || {
|
vfile=$(mktemp "${TMPDIR:-/tmp}/profile_version.XXXXXX") || {
|
||||||
disp E "Failed to create a temporary file."
|
(( quiet != 1 )) && disp E "Failed to create a temporary file."
|
||||||
return 4
|
return 4
|
||||||
}
|
}
|
||||||
|
|
||||||
dwl "$UPDT_URL/version" "$vfile" >/dev/null 2>&1 || {
|
# 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"
|
rm -f "$vfile"
|
||||||
disp E "Cannot download version file; unable to continue."
|
(( quiet != 1 )) && disp E "Cannot download version file; unable to continue."
|
||||||
return 5
|
return 5
|
||||||
}
|
}
|
||||||
|
|
||||||
if [[ -s $vfile ]]; then
|
# First line only, and only a plausible version: the text comes from the
|
||||||
lastver=$(<"$vfile")
|
# network and is displayed by disp, which interprets backslash escapes (a
|
||||||
if [[ "$lastver" != "$PROFVERSION" ]]; then
|
# crafted file, or a captive portal page, could otherwise inject terminal
|
||||||
disp I "Installed: $PROFVERSION. Available: $lastver."
|
# control sequences at every shell startup).
|
||||||
(( quiet != 1 )) && disp I "You should upgrade when possible."
|
IFS= read -r lastver < "$vfile" || true
|
||||||
result=1
|
rm -f "$vfile"
|
||||||
else
|
lastver="${lastver%$'\r'}"
|
||||||
(( quiet != 1 )) && disp I "Your version is up-to-date."
|
lastver="${lastver#"${lastver%%[![:space:]]*}"}"
|
||||||
result=0
|
lastver="${lastver%"${lastver##*[![:space:]]}"}"
|
||||||
fi
|
if [[ ! "$lastver" =~ ^[0-9]+(\.[0-9]+)*([-_.+][0-9A-Za-z._+-]+)?$ ]]; then
|
||||||
rm -f "$vfile"
|
(( 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
|
else
|
||||||
rm -f "$vfile"
|
(( quiet != 1 )) && disp I "Installed version (${PROFVERSION:-}) is more recent than the published one ($lastver)."
|
||||||
disp E "Temporary file is unreadable; unable to continue."
|
result=0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
return $result
|
return $result
|
||||||
}
|
}
|
||||||
export -f check_updates
|
export -f check_updates
|
||||||
# ------------------------------------------------------------------------------
|
|
||||||
|
|
||||||
|
|
||||||
# ------------------------------------------------------------------------------
|
# ------------------------------------------------------------------------------
|
||||||
# Apply the available profile upgrade
|
# Apply the available profile upgrade
|
||||||
# Usage: profile_upgrade [options]
|
# 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()
|
profile_upgrade()
|
||||||
{
|
{
|
||||||
local PARSED
|
local PARSED
|
||||||
@@ -134,7 +167,10 @@ profile_upgrade()
|
|||||||
disp E "Invalid options, use \"profile_upgrade --help\" to display usage."
|
disp E "Invalid options, use \"profile_upgrade --help\" to display usage."
|
||||||
return 2
|
return 2
|
||||||
fi
|
fi
|
||||||
eval set -- "$PARSED"
|
# Restore the getopt-normalised (shell-quoted) argument list
|
||||||
|
local -a _pargs=()
|
||||||
|
_getopt_split _pargs "$PARSED"
|
||||||
|
set -- "${_pargs[@]}"
|
||||||
while true; do
|
while true; do
|
||||||
case "$1" in
|
case "$1" in
|
||||||
-h|--help)
|
-h|--help)
|
||||||
@@ -148,7 +184,7 @@ profile_upgrade()
|
|||||||
printf "\t-g, --switch-to-git\tReplace current install with a fresh Git clone\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-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 "\t-F, --force\t\tDiscard local changes before upgrading\n\n"
|
||||||
printf "If the profile is installed from Git, the upgrade uses 'git pull'.\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"
|
printf "Otherwise, it downloads or applies an archive and refreshes the files.\n"
|
||||||
return 0
|
return 0
|
||||||
;;
|
;;
|
||||||
@@ -188,6 +224,13 @@ profile_upgrade()
|
|||||||
esac
|
esac
|
||||||
done
|
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
|
if (( ! use_archive && ! switch_to_git )); then
|
||||||
check_updates -q
|
check_updates -q
|
||||||
check_rc=$?
|
check_rc=$?
|
||||||
@@ -215,17 +258,18 @@ profile_upgrade()
|
|||||||
disp E "Git is required to switch this install to a Git clone."
|
disp E "Git is required to switch this install to a Git clone."
|
||||||
return 3
|
return 3
|
||||||
}
|
}
|
||||||
|
local backup_dir="${MYPATH}.pre-git.$$.bak"
|
||||||
if (( dry_run )); then
|
if (( dry_run )); then
|
||||||
disp I "[dry-run] rm -rf \"$MYPATH\"/.git"
|
disp I "[dry-run] mv \"$MYPATH\" \"$backup_dir\""
|
||||||
disp I "[dry-run] git clone $BASE_URL \"$MYPATH\""
|
disp I "[dry-run] git clone $BASE_URL \"$MYPATH\""
|
||||||
[[ -n "$branch" ]] && disp I "[dry-run] git -C \"$MYPATH\" checkout $branch"
|
[[ -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
|
return 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ -d $MYPATH/.git ]]; then
|
if [[ -d $MYPATH/.git ]]; then
|
||||||
disp W "Git repository already present; no switch is needed."
|
disp W "Git repository already present; no switch is needed."
|
||||||
else
|
else
|
||||||
local backup_dir="${MYPATH}.pre-git.$$.bak"
|
|
||||||
mv "$MYPATH" "$backup_dir" || {
|
mv "$MYPATH" "$backup_dir" || {
|
||||||
disp E "Failed to move current install out of the way."
|
disp E "Failed to move current install out of the way."
|
||||||
return 3
|
return 3
|
||||||
@@ -235,11 +279,21 @@ profile_upgrade()
|
|||||||
mv "$backup_dir" "$MYPATH" 2>/dev/null || true
|
mv "$backup_dir" "$MYPATH" 2>/dev/null || true
|
||||||
return 3
|
return 3
|
||||||
}
|
}
|
||||||
[[ -n "$branch" ]] && (
|
if [[ -n "$branch" ]] && ! ( cd "$MYPATH" && git checkout "$branch" ); then
|
||||||
cd "$MYPATH" && git checkout "$branch"
|
disp W "Could not switch to branch '$branch'; staying on the default branch."
|
||||||
) || true
|
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 "Switched installation to Git source."
|
||||||
disp I "Previous install kept in $backup_dir."
|
disp I "Previous install kept in $backup_dir (profile.conf and custom themes were copied)."
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
@@ -250,23 +304,18 @@ profile_upgrade()
|
|||||||
disp E "Git is required for this upgrade but is not available."
|
disp E "Git is required for this upgrade but is not available."
|
||||||
return 3
|
return 3
|
||||||
}
|
}
|
||||||
pushd "$MYPATH" >/dev/null || {
|
# git -C instead of cd: no directory change to undo on each exit path
|
||||||
disp E "Failed to change directory to $MYPATH."
|
local -a G=(git -C "$MYPATH")
|
||||||
return 3
|
"${G[@]}" rev-parse --is-inside-work-tree >/dev/null 2>&1 || {
|
||||||
}
|
|
||||||
git rev-parse --is-inside-work-tree >/dev/null 2>&1 || {
|
|
||||||
disp E "Install directory is not a valid Git working tree."
|
disp E "Install directory is not a valid Git working tree."
|
||||||
popd >/dev/null || return 1
|
|
||||||
return 3
|
return 3
|
||||||
}
|
}
|
||||||
|
|
||||||
# Resolve the target branch: explicit -b flag > config default > master
|
|
||||||
local target_branch="${branch:-${UPDT_DEFAULT_BRANCH:-master}}"
|
|
||||||
branch="$target_branch"
|
branch="$target_branch"
|
||||||
|
|
||||||
# Detect current local branch and warn+switch when it differs
|
# Detect current local branch and warn+switch when it differs
|
||||||
local current_branch
|
local current_branch
|
||||||
current_branch=$(git rev-parse --abbrev-ref HEAD 2>/dev/null)
|
current_branch=$("${G[@]}" rev-parse --abbrev-ref HEAD 2>/dev/null)
|
||||||
if [[ -n "$current_branch" && "$current_branch" != "$branch" ]]; then
|
if [[ -n "$current_branch" && "$current_branch" != "$branch" ]]; then
|
||||||
disp W "Branch mismatch: currently on '$current_branch', configured branch is '$branch'."
|
disp W "Branch mismatch: currently on '$current_branch', configured branch is '$branch'."
|
||||||
if (( dry_run )); then
|
if (( dry_run )); then
|
||||||
@@ -274,21 +323,20 @@ profile_upgrade()
|
|||||||
disp I "[dry-run] git checkout $branch"
|
disp I "[dry-run] git checkout $branch"
|
||||||
disp W "[dry-run] Branch would be changed from '$current_branch' to '$branch'."
|
disp W "[dry-run] Branch would be changed from '$current_branch' to '$branch'."
|
||||||
else
|
else
|
||||||
git fetch origin "$branch" || {
|
"${G[@]}" fetch origin "$branch" || {
|
||||||
disp E "Git fetch failed for branch $branch."
|
disp E "Git fetch failed for branch $branch."
|
||||||
popd >/dev/null || return 1
|
|
||||||
return 2
|
return 2
|
||||||
}
|
}
|
||||||
git checkout "$branch" || {
|
"${G[@]}" checkout "$branch" || {
|
||||||
disp E "Git checkout failed for branch $branch."
|
disp E "Git checkout failed for branch $branch."
|
||||||
popd >/dev/null || return 1
|
|
||||||
return 2
|
return 2
|
||||||
}
|
}
|
||||||
disp W "Branch has been changed from '$current_branch' to '$branch'."
|
disp W "Branch has been changed from '$current_branch' to '$branch'."
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if ! git diff --quiet || ! git diff --cached --quiet || [[ -n $(git ls-files --others --exclude-standard) ]]; then
|
# status --porcelain: staged, unstaged and untracked (not ignored) files
|
||||||
|
if [[ -n $("${G[@]}" status --porcelain 2>/dev/null) ]]; then
|
||||||
if (( force_git )); then
|
if (( force_git )); then
|
||||||
disp W "Force mode: local Git changes and untracked files will be lost."
|
disp W "Force mode: local Git changes and untracked files will be lost."
|
||||||
if (( dry_run )); then
|
if (( dry_run )); then
|
||||||
@@ -296,19 +344,16 @@ profile_upgrade()
|
|||||||
disp I "[dry-run] git reset --hard HEAD"
|
disp I "[dry-run] git reset --hard HEAD"
|
||||||
disp I "[dry-run] git clean -fd"
|
disp I "[dry-run] git clean -fd"
|
||||||
else
|
else
|
||||||
git fetch --all --prune || {
|
"${G[@]}" fetch --all --prune || {
|
||||||
disp E "Git fetch failed, upgrade not applied."
|
disp E "Git fetch failed, upgrade not applied."
|
||||||
popd >/dev/null || return 1
|
|
||||||
return 4
|
return 4
|
||||||
}
|
}
|
||||||
git reset --hard HEAD || {
|
"${G[@]}" reset --hard HEAD || {
|
||||||
disp E "Git reset failed, upgrade not applied."
|
disp E "Git reset failed, upgrade not applied."
|
||||||
popd >/dev/null || return 1
|
|
||||||
return 4
|
return 4
|
||||||
}
|
}
|
||||||
git clean -fd || {
|
"${G[@]}" clean -fd || {
|
||||||
disp E "Git clean failed, upgrade not applied."
|
disp E "Git clean failed, upgrade not applied."
|
||||||
popd >/dev/null || return 1
|
|
||||||
return 4
|
return 4
|
||||||
}
|
}
|
||||||
fi
|
fi
|
||||||
@@ -319,16 +364,16 @@ profile_upgrade()
|
|||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
if (( dry_run )); then
|
if (( dry_run )); then
|
||||||
disp I "[dry-run] git pull origin $branch"
|
disp I "[dry-run] git pull --ff-only origin $branch"
|
||||||
else
|
else
|
||||||
git pull origin "$branch" || {
|
# --ff-only: never create a merge commit nor leave conflict markers
|
||||||
disp E "Git pull failed, upgrade not applied."
|
# in the installed scripts; local commits that diverge are reported
|
||||||
popd >/dev/null || return 1
|
"${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
|
return 2
|
||||||
}
|
}
|
||||||
disp I "Successfully upgraded using git."
|
disp I "Successfully upgraded using git. Please log out and log in again."
|
||||||
fi
|
fi
|
||||||
popd >/dev/null || return 1
|
|
||||||
else
|
else
|
||||||
if (( use_archive )); then
|
if (( use_archive )); then
|
||||||
[[ -r "$archive_file" ]] || {
|
[[ -r "$archive_file" ]] || {
|
||||||
@@ -375,7 +420,7 @@ profile_upgrade()
|
|||||||
if (( dry_run )); then
|
if (( dry_run )); then
|
||||||
disp I "[dry-run] dwl \"$ARCH_URL\" \"$archive\""
|
disp I "[dry-run] dwl \"$ARCH_URL\" \"$archive\""
|
||||||
else
|
else
|
||||||
dwl "$ARCH_URL" "$archive" || {
|
dwl -t 300 "$ARCH_URL" "$archive" || {
|
||||||
disp E "Failed to download archive."
|
disp E "Failed to download archive."
|
||||||
rm -rf "$tmpdir"
|
rm -rf "$tmpdir"
|
||||||
return 6
|
return 6
|
||||||
@@ -384,10 +429,25 @@ profile_upgrade()
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
if (( dry_run )); then
|
if (( dry_run )); then
|
||||||
disp I "[dry-run] tar -xzf \"$archive\" -C \"$tmpdir\""
|
disp I "[dry-run] tar -xf \"$archive\" -C \"$tmpdir\""
|
||||||
disp I "[dry-run] cp -a <extracted_profile>/. \"$MYPATH\"/"
|
disp I "[dry-run] cp -a <extracted_profile>/. \"$MYPATH\"/"
|
||||||
else
|
else
|
||||||
tar -xzf "$archive" -C "$tmpdir" || {
|
# 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."
|
disp E "Archive extraction failed."
|
||||||
rm -rf "$tmpdir"
|
rm -rf "$tmpdir"
|
||||||
return 7
|
return 7
|
||||||
@@ -400,6 +460,16 @@ profile_upgrade()
|
|||||||
return 8
|
return 8
|
||||||
fi
|
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..."
|
disp I "Installing new version..."
|
||||||
cp -a "$extracted_root"/. "$MYPATH"/ || {
|
cp -a "$extracted_root"/. "$MYPATH"/ || {
|
||||||
disp E "Failed to copy new files into $MYPATH."
|
disp E "Failed to copy new files into $MYPATH."
|
||||||
@@ -407,13 +477,16 @@ profile_upgrade()
|
|||||||
return 9
|
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."
|
disp I "Upgrade complete. Please log out and log in again."
|
||||||
rm -rf "$tmpdir"
|
rm -rf "$tmpdir"
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
return 0
|
||||||
}
|
}
|
||||||
export -f profile_upgrade
|
export -f profile_upgrade
|
||||||
# ------------------------------------------------------------------------------
|
# ------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
# EOF
|
# EOF
|
||||||
|
|||||||
+319
-42
@@ -35,17 +35,180 @@
|
|||||||
# * OF SUCH DAMAGE.
|
# * OF SUCH DAMAGE.
|
||||||
# ------------------------------------------------------------------------------
|
# ------------------------------------------------------------------------------
|
||||||
|
|
||||||
if [[ ! $SHELL =~ bash ]]; then
|
# This first test is POSIX on purpose: it runs before anything bash-specific, so
|
||||||
echo "That environment script is designed to be used with bash being the shell."
|
# that sourcing the file from dash, zsh, ... ends with a message instead of
|
||||||
echo "Please consider using bash to enjoy our features!"
|
# 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
|
||||||
|
|
||||||
|
_profile_is_sourced()
|
||||||
|
{
|
||||||
|
[[ "${BASH_SOURCE[0]}" != "$0" ]]
|
||||||
|
}
|
||||||
|
|
||||||
|
# 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
|
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
|
fi
|
||||||
|
|
||||||
# Required for associative arrays (4.0+) and namerefs (4.3+)
|
# 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
|
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."
|
echo "[ Error ] This profile requires Bash 4.3 or higher." >&2
|
||||||
echo "Current version: $BASH_VERSION"
|
echo "Current version: $BASH_VERSION" >&2
|
||||||
(return 0 2>/dev/null) && return 1 || exit 1
|
return 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# ------------------------------------------------------------------------------
|
# ------------------------------------------------------------------------------
|
||||||
@@ -78,6 +241,10 @@ pathremove()
|
|||||||
# export "$pathvar=$1${!pathvar:+:${!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()
|
pathappend()
|
||||||
{
|
{
|
||||||
[[ -z "$1" ]] && return 0
|
[[ -z "$1" ]] && return 0
|
||||||
@@ -86,7 +253,9 @@ pathappend()
|
|||||||
printf "pathappend: unsafe variable name '%s'\n" "$pathvar" >&2
|
printf "pathappend: unsafe variable name '%s'\n" "$pathvar" >&2
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
pathremove "$1" "$pathvar"
|
case ":${!pathvar}:" in
|
||||||
|
*":$1:"*) return 0 ;;
|
||||||
|
esac
|
||||||
export "$pathvar=${!pathvar:+${!pathvar}:}$1"
|
export "$pathvar=${!pathvar:+${!pathvar}:}$1"
|
||||||
}
|
}
|
||||||
# ------------------------------------------------------------------------------
|
# ------------------------------------------------------------------------------
|
||||||
@@ -111,13 +280,21 @@ parse_conf()
|
|||||||
# Skip comments and empty lines
|
# Skip comments and empty lines
|
||||||
[[ -z "$key" || "$key" =~ ^[#\;] ]] && continue
|
[[ -z "$key" || "$key" =~ ^[#\;] ]] && continue
|
||||||
|
|
||||||
# Section Detection: [section_name]
|
# Section Detection: [section_name], optionally followed by a comment
|
||||||
if [[ "$key" =~ ^\[([a-zA-Z0-9_]+)\]$ ]]; then
|
if [[ "$key" =~ ^\[([a-zA-Z0-9_]+)\][[:space:]]*([#\;].*)?$ ]]; then
|
||||||
current_section="${BASH_REMATCH[1]}"
|
current_section="${BASH_REMATCH[1]}"
|
||||||
declare -g -A "CONF_$current_section"
|
declare -g -A "CONF_$current_section"
|
||||||
continue
|
continue
|
||||||
fi
|
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)
|
# Secure Assignment (if inside a section)
|
||||||
if [[ -n "$current_section" ]]; then
|
if [[ -n "$current_section" ]]; then
|
||||||
# Clean the value
|
# Clean the value
|
||||||
@@ -125,19 +302,44 @@ parse_conf()
|
|||||||
value="${value#"${value%%[![:space:]]*}"}"
|
value="${value#"${value%%[![:space:]]*}"}"
|
||||||
value="${value%$'\r'}"
|
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
|
# Protect against command injection by disallowing certain characters in keys
|
||||||
value="${value//\`/}"
|
value="${value//\`/}"
|
||||||
value="${value//\$\(/}"
|
value="${value//\$\(/}"
|
||||||
|
|
||||||
# Correctly interpretet internal variables (e.g. $HOME)
|
# 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 [[ "$value" == *\$* ]]; then
|
||||||
value=$(envsubst <<< "$value")
|
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
|
fi
|
||||||
|
|
||||||
# Strip quotes (handling both " and ')
|
|
||||||
value="${value%\"}"; value="${value#\"}"
|
|
||||||
value="${value%\'}"; value="${value#\'}"
|
|
||||||
|
|
||||||
# Use a nameref for safe, eval-free assignment
|
# Use a nameref for safe, eval-free assignment
|
||||||
local -n current_array="CONF_$current_section"
|
local -n current_array="CONF_$current_section"
|
||||||
# shellcheck disable=SC2034 # Dynamic var creation
|
# shellcheck disable=SC2034 # Dynamic var creation
|
||||||
@@ -159,6 +361,7 @@ load_alias()
|
|||||||
|
|
||||||
# Create a nameref to the section array
|
# Create a nameref to the section array
|
||||||
local -n current_aliases="$section_name"
|
local -n current_aliases="$section_name"
|
||||||
|
local key
|
||||||
|
|
||||||
# Iterate safely over the keys of the associative array
|
# Iterate safely over the keys of the associative array
|
||||||
for key in "${!current_aliases[@]}"; do
|
for key in "${!current_aliases[@]}"; do
|
||||||
@@ -187,6 +390,7 @@ load_conf()
|
|||||||
[[ "$(declare -p "$section_name" 2>/dev/null)" != "declare -A"* ]] && return 0
|
[[ "$(declare -p "$section_name" 2>/dev/null)" != "declare -A"* ]] && return 0
|
||||||
|
|
||||||
local -n current_vars="$section_name"
|
local -n current_vars="$section_name"
|
||||||
|
local key
|
||||||
|
|
||||||
for key in "${!current_vars[@]}"; do
|
for key in "${!current_vars[@]}"; do
|
||||||
# Export the key/value pair as a standard shell variable
|
# Export the key/value pair as a standard shell variable
|
||||||
@@ -203,32 +407,41 @@ load_conf()
|
|||||||
# ------------------------------------------------------------------------------
|
# ------------------------------------------------------------------------------
|
||||||
# ------------------------------------------------------------------------------
|
# ------------------------------------------------------------------------------
|
||||||
|
|
||||||
# Store script's path (realpath -s resolve symlinks if profile.sh is a symlink)
|
# Store the script's path. Because we're more likely to be sourced, we use
|
||||||
# Because we're more likely to be sourced, we use BASH_SOURCE to get the path
|
# BASH_SOURCE to get the path of the sourced file instead of $0. If profile.sh
|
||||||
# of the sourced file instead of $0
|
# itself is a symbolic link, it is followed to find the real installation
|
||||||
if [[ -z "$PROFILE_PATH" ]]; then
|
# (profile.d, version, ...); a symlinked directory is kept as the user sees it
|
||||||
MYPATH=$(dirname "$(realpath -s "${BASH_SOURCE[0]}")")
|
# (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
|
else
|
||||||
MYPATH="$PROFILE_PATH"
|
MYPATH="$PROFILE_PATH"
|
||||||
fi
|
fi
|
||||||
export MYPATH
|
export MYPATH
|
||||||
if [[ ! -e "$MYPATH/profile.sh" ]]; then
|
if [[ ! -e "$MYPATH/profile.sh" ]]; then
|
||||||
echo "[ Warning ] Path detection failed, trying to use pwd..."
|
echo "[ Warning ] Path detection failed, trying to use pwd..." >&2
|
||||||
MYPATH=$(pwd)
|
MYPATH=$(pwd)
|
||||||
if [[ ! -e "$MYPATH/profile.sh" ]]; then
|
if [[ ! -e "$MYPATH/profile.sh" ]]; then
|
||||||
echo "[ Error ] Unable to determine installation path, pretty much nothing will work."
|
echo "[ Error ] Unable to determine installation path, pretty much nothing will work." >&2
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ ! -s "$MYPATH/version" ]]; then
|
PROFVERSION=""
|
||||||
echo "[ Warning ] Unable to determine running profile version; your installation might be broken."
|
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
|
fi
|
||||||
PROFVERSION=$(cat "$MYPATH"/version)
|
|
||||||
export PROFVERSION
|
export PROFVERSION
|
||||||
|
|
||||||
# Build PATH environment variable
|
# Build PATH environment variable
|
||||||
if [[ $EUID -eq 0 ]]; then
|
if [[ $EUID -eq 0 ]]; then
|
||||||
pathappend /sbin:/usr/sbin
|
pathappend /sbin
|
||||||
|
pathappend /usr/sbin
|
||||||
fi
|
fi
|
||||||
[[ -d ~/bin ]] && pathappend ~/bin
|
[[ -d ~/bin ]] && pathappend ~/bin
|
||||||
[[ -d ~/.local/bin ]] && pathappend ~/.local/bin
|
[[ -d ~/.local/bin ]] && pathappend ~/.local/bin
|
||||||
@@ -236,32 +449,93 @@ fi
|
|||||||
# Parse and load general configuration
|
# Parse and load general configuration
|
||||||
export PROFILE_CONF="$MYPATH/profile.conf"
|
export PROFILE_CONF="$MYPATH/profile.conf"
|
||||||
parse_conf "$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 system # Load Bash system behavior configuration (history, pager, etc.)
|
||||||
load_conf general # General purpose configuration (compilation flags, etc.)
|
load_conf general # General purpose configuration (compilation flags, etc.)
|
||||||
|
|
||||||
# Load module scripts
|
# 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
|
shopt -s nullglob
|
||||||
for script in "$MYPATH/profile.d/"*.sh; do
|
for _profile_script in "$MYPATH/profile.d/"*.sh; do
|
||||||
if [[ -f "$script" && -r "$script" ]]; then
|
if [[ -f "$_profile_script" && -r "$_profile_script" ]]; then
|
||||||
# shellcheck source=/dev/null
|
# shellcheck source=/dev/null
|
||||||
. "$script" || printf "[ Warning ] Failed to source module: %s\n" "$script" >&2
|
. "$_profile_script" || printf "[ Warning ] Failed to source module: %s\n" "$_profile_script" >&2
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
shopt -u nullglob
|
unset _profile_script
|
||||||
|
(( _profile_ng )) || shopt -u nullglob
|
||||||
|
|
||||||
# Interactive shell detection, two methods available each one of those might have different result
|
# Interactive shell detection. INTERACTIVE is exported for the modules, so it
|
||||||
# depending on distribution
|
# is reset when the shell is not interactive: a non-interactive child of an
|
||||||
#shopt -q login_shell && INTERACTIVE=1
|
# interactive shell would otherwise inherit INTERACTIVE=1 and run the whole
|
||||||
[[ $- == *i* ]] && export INTERACTIVE=1
|
# interactive part (banner, prompt, update check) inside scripts.
|
||||||
|
if [[ $- == *i* ]]; then
|
||||||
|
export INTERACTIVE=1
|
||||||
|
else
|
||||||
|
unset INTERACTIVE
|
||||||
|
fi
|
||||||
|
|
||||||
if [[ $INTERACTIVE ]]; then
|
if [[ -n "${INTERACTIVE:-}" ]]; then
|
||||||
# For compiling (as we often compile with LFS/0linux...)
|
# Load custom bash completions
|
||||||
#Aliases
|
shopt -s nullglob
|
||||||
load_alias aliases
|
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
|
||||||
|
|
||||||
# Define PS1
|
# 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
|
trap 'timer_start' DEBUG
|
||||||
PROMPT_COMMAND='set_prompt'
|
|
||||||
|
# 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]).
|
# 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 value must match one of the alias names defined in SET_LOCALE so that
|
||||||
@@ -279,9 +553,12 @@ if [[ $INTERACTIVE ]]; then
|
|||||||
check_updates -q
|
check_updates -q
|
||||||
disp I "Profile version $PROFVERSION loaded..."
|
disp I "Profile version $PROFVERSION loaded..."
|
||||||
fi
|
fi
|
||||||
|
unset _profile_ng
|
||||||
|
|
||||||
# Cleanup
|
# 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
|
#return 0
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user