Compare commits
5 Commits
ee72ede116
...
28e4c112af
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
28e4c112af | ||
|
|
9ec52aa49f | ||
|
|
1b28e90c62 | ||
|
|
1e31712b60 | ||
|
|
5faae67d11 |
22
README.md
22
README.md
@@ -42,7 +42,22 @@ 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
|
||||||
@@ -90,6 +105,7 @@ A bar-style prompt showing current time, execution time of the last command
|
|||||||
| `ku` | processes | Kill all processes owned by the given user name or ID |
|
| `ku` | processes | Kill all processes owned by the given user name or ID |
|
||||||
| `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 in terminal with colors, inline formatting, and framed code blocks |
|
||||||
| `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 |
|
||||||
| `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`) |
|
||||||
@@ -132,7 +148,7 @@ apply when unset.
|
|||||||
|
|
||||||
`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
|
||||||
|
|
||||||
@@ -152,7 +168,7 @@ change the default without having to pass flags every time.
|
|||||||
| 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` | `tar.gz` | Archive format for `taz` (`tar.gz`, `tar.bz2`, `tar.xz`, `zip`, …) |
|
||||||
| `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` | `0` | Set to `1` to always extract into a subdirectory |
|
||||||
|
|||||||
@@ -23,6 +23,11 @@ Versions follow `MAJOR.MINOR.PATCH-REVISION_STAGE_N` (e.g. `3.99.1-4_rc_1`).
|
|||||||
- `disp` now wraps long messages on terminal width, avoids mid-word splits, and
|
- `disp` now wraps long messages on terminal width, avoids mid-word splits, and
|
||||||
aligns continuation lines with the message body after the prefix.
|
aligns continuation lines with the message body after the prefix.
|
||||||
- `help` now supports `help <command>` and delegates to `<command> --help`.
|
- `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`.
|
||||||
|
- `taz` keeps backward compatibility with legacy `TAZ_DEFAULT_THREADS=0`
|
||||||
|
values by interpreting `0` as `auto`.
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
- Startup responsiveness improved: `check_updates -q` now uses a short network
|
- Startup responsiveness improved: `check_updates -q` now uses a short network
|
||||||
|
|||||||
27
doc/FAQ.md
27
doc/FAQ.md
@@ -52,6 +52,33 @@ 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.
|
||||||
|
|
||||||
|
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`:
|
||||||
|
|||||||
@@ -30,8 +30,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
|
||||||
|
|||||||
@@ -395,18 +395,32 @@ 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)
|
# -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()
|
||||||
{
|
{
|
||||||
|
# Resolve runtime CPU count for --parallel=auto.
|
||||||
|
_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"
|
||||||
|
}
|
||||||
|
|
||||||
# shellcheck disable=SC2329
|
# shellcheck disable=SC2329
|
||||||
_doxz()
|
_doxz()
|
||||||
{
|
{
|
||||||
@@ -537,13 +551,13 @@ taz()
|
|||||||
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"
|
||||||
printf "\t\t\tgiven, the smalest is kept\n"
|
printf "\t\t\tgiven, the smalest is kept\n"
|
||||||
printf "\t-p, --parallel\tNumber of threads to use (if allowed by underlying utility)\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"
|
||||||
@@ -606,11 +620,20 @@ taz()
|
|||||||
[[ ${#FILES[@]} -eq 0 ]] && FILES=(".")
|
[[ ${#FILES[@]} -eq 0 ]] && FILES=(".")
|
||||||
|
|
||||||
[[ ! $compform ]] && compform=${TAZ_DEFAULT_FORMAT:-lz}
|
[[ ! $compform ]] && compform=${TAZ_DEFAULT_FORMAT:-lz}
|
||||||
[[ ! $nproc ]] && nproc=${TAZ_DEFAULT_THREADS:-1}
|
[[ ! $nproc ]] && nproc=${TAZ_DEFAULT_THREADS:-auto}
|
||||||
[[ ! $complevel ]] && complevel=${TAZ_DEFAULT_LEVEL:-6}
|
[[ ! $complevel ]] && complevel=${TAZ_DEFAULT_LEVEL:-6}
|
||||||
[[ $verbose -gt 1 && $quiet -gt 1 ]] &&
|
[[ $verbose -gt 1 && $quiet -gt 1 ]] &&
|
||||||
disp E "The --verbose and --quiet options can't be used together."
|
disp E "The --verbose and --quiet options can't be used together."
|
||||||
|
|
||||||
|
# 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
|
||||||
|
|
||||||
for item in "${FILES[@]}"; do
|
for item in "${FILES[@]}"; do
|
||||||
local donetar=0
|
local donetar=0
|
||||||
disp I "Processing $item..."
|
disp I "Processing $item..."
|
||||||
|
|||||||
@@ -236,6 +236,478 @@ export -f disp
|
|||||||
# ------------------------------------------------------------------------------
|
# ------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
# Render Markdown files with terminal formatting
|
||||||
|
# Usage: mdcat [file]
|
||||||
|
mdcat()
|
||||||
|
{
|
||||||
|
_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
|
||||||
|
|
||||||
|
# Apply inline transforms in a safe order: code, links, then emphasis.
|
||||||
|
# This prevents emphasis parsing inside code spans or link URLs.
|
||||||
|
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}${code_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}${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
|
||||||
|
|
||||||
|
# Unescape Markdown punctuation escapes, such as \<, \>, \_, and \*.
|
||||||
|
while [[ "$text" =~ \\([[:punct:]]) ]]; do
|
||||||
|
local match="${BASH_REMATCH[0]}"
|
||||||
|
local val="${BASH_REMATCH[1]}"
|
||||||
|
local before="${text%%"$match"*}"
|
||||||
|
local after="${text#*"$match"}"
|
||||||
|
text="${before}${val}${after}"
|
||||||
|
done
|
||||||
|
|
||||||
|
printf "%s\n" "$text"
|
||||||
|
}
|
||||||
|
|
||||||
|
_mdcat_print_hr()
|
||||||
|
{
|
||||||
|
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
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
_mdcat_print_code_block()
|
||||||
|
{
|
||||||
|
local lang="$1"
|
||||||
|
shift
|
||||||
|
local -a lines=("$@")
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
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"
|
||||||
|
}
|
||||||
|
|
||||||
|
_mdcat_print_table()
|
||||||
|
{
|
||||||
|
local -a lines=("$@")
|
||||||
|
local -a table_rows=()
|
||||||
|
local -a col_widths=()
|
||||||
|
local i j ncols=0
|
||||||
|
local sep=$'\x1f'
|
||||||
|
|
||||||
|
_mdcat_parse_table_row()
|
||||||
|
{
|
||||||
|
local input="$1"
|
||||||
|
local line
|
||||||
|
line=$(printf '%s' "$input" | sed -E 's/^[[:space:]]+//; s/[[: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=$(printf '%s' "$cell" | sed -E 's/^[[:space:]]+//; s/[[: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"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Parse header and data rows, skipping the Markdown separator row.
|
||||||
|
# Width is computed from visible text length with ANSI escapes stripped.
|
||||||
|
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 vis
|
||||||
|
vis=$(_mdcat_style_inline "${row[j]}")
|
||||||
|
vis=$(printf '%b' "$vis" | sed -E 's/\x1B\[[0-9;]*[mK]//g')
|
||||||
|
local cell_len=${#vis}
|
||||||
|
[[ -z "${col_widths[j]}" ]] && col_widths[j]=0
|
||||||
|
(( col_widths[j] < cell_len )) && col_widths[j]=$cell_len
|
||||||
|
done
|
||||||
|
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
|
||||||
|
|
||||||
|
# Draw top border
|
||||||
|
local border="+"
|
||||||
|
for ((j=0; j<ncols; ++j)); do
|
||||||
|
local w=$((col_widths[j]+2))
|
||||||
|
border+="$(printf '%*s' "$w" "" | tr ' ' '-')+"
|
||||||
|
done
|
||||||
|
printf "%b\n" "$IBlack$border$RESETCOL"
|
||||||
|
|
||||||
|
# Print header row
|
||||||
|
local -a header=()
|
||||||
|
IFS="$sep" read -r -a header <<< "${table_rows[0]}"
|
||||||
|
printf "%b|" "$IBlack"
|
||||||
|
for ((j=0; j<ncols; ++j)); do
|
||||||
|
local raw_cell="${header[j]:-}"
|
||||||
|
local styled_cell
|
||||||
|
styled_cell=$(_mdcat_style_inline "$raw_cell")
|
||||||
|
local visible
|
||||||
|
visible=$(printf '%b' "$styled_cell" | sed -E 's/\x1B\[[0-9;]*[mK]//g')
|
||||||
|
local pad=$((col_widths[j] - ${#visible}))
|
||||||
|
(( pad < 0 )) && pad=0
|
||||||
|
printf " %b%b%*s%b |" "$BBlue" "$styled_cell" "$pad" "" "$IBlack"
|
||||||
|
done
|
||||||
|
printf "%b\n" "$RESETCOL"
|
||||||
|
|
||||||
|
# Header separator
|
||||||
|
printf "%b|" "$IBlack"
|
||||||
|
for ((j=0; j<ncols; ++j)); do
|
||||||
|
printf " %s |" "$(printf '%*s' "${col_widths[j]}" "" | tr ' ' '-')"
|
||||||
|
done
|
||||||
|
printf "%b\n" "$RESETCOL"
|
||||||
|
|
||||||
|
# Print data rows
|
||||||
|
for ((i=1; i<${#table_rows[@]}; ++i)); do
|
||||||
|
local -a row=()
|
||||||
|
IFS="$sep" read -r -a row <<< "${table_rows[i]}"
|
||||||
|
printf "%b|" "$IBlack"
|
||||||
|
for ((j=0; j<ncols; ++j)); do
|
||||||
|
local raw_cell="${row[j]:-}"
|
||||||
|
local styled_cell
|
||||||
|
styled_cell=$(_mdcat_style_inline "$raw_cell")
|
||||||
|
local visible
|
||||||
|
visible=$(printf '%b' "$styled_cell" | sed -E 's/\x1B\[[0-9;]*[mK]//g')
|
||||||
|
local pad=$((col_widths[j] - ${#visible}))
|
||||||
|
(( pad < 0 )) && pad=0
|
||||||
|
printf " %b%b%*s%b |" "$RESETCOL" "$styled_cell" "$pad" "" "$IBlack"
|
||||||
|
done
|
||||||
|
printf "%b\n" "$RESETCOL"
|
||||||
|
done
|
||||||
|
|
||||||
|
# Draw bottom border
|
||||||
|
printf "%b\n" "$IBlack$border$RESETCOL"
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
eval set -- "$PARSED"
|
||||||
|
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=()
|
||||||
|
while IFS= read -r raw || [[ -n "$raw" ]]; do
|
||||||
|
line="${raw%$'\r'}"
|
||||||
|
|
||||||
|
# Table detection: line with |, next line with | and ---
|
||||||
|
if [[ $in_table -eq 0 && "$line" =~ ^[[:space:]]*\|.*\|[[:space:]]*$ ]]; then
|
||||||
|
local next
|
||||||
|
IFS= read -r next || true
|
||||||
|
# Accept: | --- | --- | or |:---|---:| etc.
|
||||||
|
if [[ "$next" =~ ^[[:space:]]*\|[[:space:]]*:?[-]+:?([[:space:]]*\|[[:space:]]*:?[ -]+:?)*\|[[:space:]]*$ ]]; then
|
||||||
|
in_table=1
|
||||||
|
table_lines=("$line" "$next")
|
||||||
|
continue
|
||||||
|
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
|
||||||
|
printf "%b%s%b\n" "$h_on" "$title" "$RESETCOL"
|
||||||
|
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}"
|
||||||
|
|
||||||
|
if [[ $in_code -eq 1 ]]; then
|
||||||
|
_mdcat_print_code_block "$code_lang" "${code_lines[@]}"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
export -f mdcat
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
# Load disp section variables
|
# Load disp section variables
|
||||||
load_conf disp
|
load_conf disp
|
||||||
set_colors
|
set_colors
|
||||||
|
|||||||
@@ -79,6 +79,7 @@ help()
|
|||||||
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 "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 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 "pkgs\t\tSearch for a pattern in installed package names (dpkg/rpm, supports -i)\n"
|
||||||
@@ -102,7 +103,7 @@ help()
|
|||||||
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\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
|
||||||
# ------------------------------------------------------------------------------
|
# ------------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -161,8 +161,6 @@ if ((BASH_VERSINFO[0] < 4)) || [[ ${BASH_VERSINFO[0]} -eq 4 && ${BASH_VERSINFO[1
|
|||||||
(return 0 2>/dev/null) && return 1 || exit 1
|
(return 0 2>/dev/null) && return 1 || exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
unset -f _profile_is_sourced _profile_finish _profile_install_in_file _profile_install
|
|
||||||
|
|
||||||
# ------------------------------------------------------------------------------
|
# ------------------------------------------------------------------------------
|
||||||
# path* : private functions for PATH variable management
|
# path* : private functions for PATH variable management
|
||||||
pathremove()
|
pathremove()
|
||||||
@@ -408,7 +406,9 @@ if [[ $INTERACTIVE ]]; then
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
# Cleanup
|
# Cleanup
|
||||||
unset pathremove pathprepend pathappend
|
unset -f _profile_is_sourced _profile_finish _profile_install_in_file _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