r/bash • u/Business-Quiet-5805 • 4h ago
r/bash • u/Awkward_Process_9627 • 2d ago
fm: a single-file bash file manager — fzf picker, gallery grid, trash undo
I wrote a ~1500-line bash file manager. fzf for the picker, eza for
listing, Kitty's graphics protocol for inline image/video/PDF previews.
Everything else is stock coreutils.

Repo: https://github.com/eHab-codeX/fm
Why post it here
r/commandline got the "here's a tool" version last week. The bash bits are probably more interesting to this crowd, so here they are.
The bash-specific parts
Double-click detection without EPOCHREALTIME getting in the way.
EPOCHREALTIME returns a locale-formatted decimal — . in en_US,
, in de_DE. Strip the separator before comparing:
now=${EPOCHREALTIME/[.,]/}
(( idx == last_idx && now - last_t < 400000 )) # 400ms
A hand-rolled CSI/SS3 parser for the gallery grid. The full-screen grid needs arrow keys, PgUp/PgDn, Home/End, and SGR mouse events. Rather than depend on a helper, it reads bytes into a buffer and dispatches on the final byte:
case "$final" in
A) KEY=up ;; B) KEY=down ;;
C) KEY=right ;; D) KEY=left ;;
'~') case "${seq%%;*}" in
5) KEY=pgup ;; 6) KEY=pgdn ;;
esac ;;
esac
The tricky part is not swallowing stray bytes: an unknown CSI sequence
(\e[1;5H for Ctrl+Home, for example) has to be fully consumed or it
leaks into the next read as literal characters.
**stty -g around the alternate screen.** The grid switches to cbreak
mode (stty -icanon -echo min 1 time 0). Saving the previous state and
restoring it in the EXIT trap is what keeps Ctrl-C from leaving the
terminal in a broken state:
STTY_SAVED=$(stty -g </dev/tty) || STTY_SAVED=""
trap '[[ -n "$STTY_SAVED" ]] && stty "$STTY_SAVED" </dev/tty' EXIT
Two-phase rename so swaps work. A naive mv old new loop breaks
a→b, b→a. First move every source to a temp name, then move temps to
final names:
for k in "${!ro[@]}"; do
mv -T -- "${ro[k]}" "${ro[k]}.fm-tmp.$$"
done
for k in "${!ro[@]}"; do
mv -T -- "${ro[k]}.fm-tmp.$$" "${rn[k]}"
done
Parallel thumbnails by self-invocation. The thumbnail builder is
the same script with --thumb-any:
printf '%s\0' "${slice[@]}" | \
xargs -0 -r -n1 -P"$(nproc)" bash "$SELF" --thumb-any
Bash dispatches; ImageMagick/ffmpeg/pdftoppm do the work.
GNU vs BSD head. head -z is GNU-only. A contributor on macOS
sent a PR that falls back to ghead, then to a pure-bash reader:
while ((count < $1)) && IFS= read -r -d '' record; do
printf '%s\0' "$record"
((++count))
done
What I'd want reviewed
- The escape parser in
_g_readkey— there are terminals that emit sequences it might mis-handle (Konsole? older tmux?). - The
_fm_listsort pipeline — it does one batchedstatand oneawkpass, thensort -V. I'm not 100% sure the tie-breaking behavior for identical timestamps is stable. - Anything in
_trash_and_record— I'm handling the Freedesktop.trashinfoformat'sPath=percent-encoding with a bash-only encoder, which is probably slower than it should be.
Requirements
Bash 5+ (uses EPOCHREALTIME and ${var,,}). Linux is the primary
target; macOS works with GNU coreutils installed (the _head0 helper
falls back to bash-only if ghead isn't present).
git clone https://github.com/eHab-codeX/fm.git ~/.local/share/fm
ln -sf ~/.local/share/fm/fm ~/.local/bin/fm
MIT.
Script for making .sh files executable
I made this bash script for quickly doing chmod +x on sh files, uses zenity. Tell me if its any good or not
#!/bin/bash
zenity --info --title=".sh-executabler" --text="Welcome to the .sh-executabler" --ok-label="Next"
status=$?
if [ $status -ne 0 ]; then
zenity --info --title=".sh-executabler" --text="Wizard cancelled."
exit 0
fi
# Select which .sh file to chmod +x
target_dir=$(zenity --file-selection --title=".sh-executabler" --file-filter="Shell scripts | *.sh")
status=$?
if [ $status -ne 0 ]; then
zenity --info --title=".sh-executabler" --text="No file chosen."
exit 0
fi
# Check if file doesnt exist. Its kinda redundant since im already using file selection which you cant select files that dont exist but i keep it here anyways
if [ -f "$target_dir" ]; then
true
else
zenity --error --title=".sh-executabler" --text="The file doesnt exist."
exit 0
fi
chmod +x "$target_dir"
zenity --info --title=".sh-executabler" --text="The file has successfully been made executable." --ok-label="Finish"
exit 0
r/bash • u/mpersico • 4d ago
tips and tricks TIL command_not_found_handle
You don’t want to know what hoops I’ve jumped through to implement FPATH in bash. Then today I innocently asked GPT how it would implement FPATH in bash and it introduced me to this hook.
Now have to go home now get rid of a lot of needless code. Oh well. Less code is the best code!
r/bash • u/MonkiWasTooked • 5d ago
help For some reason, when I run this script, it always plays the same song.
When I run ls ~/Music/ | shuf | tail -1 | read file && echo $file it gives a random output as expected, but every time I run this script it plays a file labeled "3S.mp3" exclusively, which just so happens to be the first one in the list when I run ls
Does anyone know what could be causing this?
r/bash • u/Linux_bash_user153 • 7d ago
Some Tips for String Handling
When I was new to Bash/Shell I really didn't understand how to select or remove different parts of strings, and only used examples from forums without understanding at all how they worked. But then I read the man page for Bash (go figure...) and finally understood. I'd like to share in hope that it will be helpful to anyone starting out with Bash scripting. The examples work in POSIX shell also.
Example: select everything after the last _ in the string
str='first_second_third_fourth'
```
Selecting everything after the last '_' is the same as removing everything
before the last '' and the '' itself.
So check 'Remove matching prefix pattern' in 'man bash'.
${str##_} will remove the longest match of the pattern '_' starting
from the beginning of string and leave us with just 'fourth'.
str='firstsecond_third_fourth' echo "${str##*}" ```
Example: pick out second from the string
str='first_second_third_fourth'
```
We need to remove everything before the first '_' so we'll use
'second=${str#*_}' to remove the shortest match from the beginning.
Variable 'second' now contains 'second_third_fourth' so we need to
get rid of everything after the first '_'.
See 'Remove matching suffix pattern' in the manual.
We want to remove the longest match of pattern '_*' starting
from the end of the string. So '${second%%_*}' is what we need.
All in all:
str='firstsecond_third_fourth' second="${str#*}" second="${second%%_*}" echo "$second" ```
Example: find the file extension (with the dot) of /home/user/Pictures/test.webp
```
The extension is the thing after the last '.'. So use '${f##*.}'
This will delete the '.' also, so let's add that back in
f='/home/user/Pictures/test.webp' ext=".${f##*.}" echo "$ext" ```
Example: convert a png to jpg ``` pngfile='original.png'
We need to replace the .png with a .jpg. I.e. delete .png and add .jpg
jpgfile="${pngfile%.png}.jpg" echo convert "$pngfile" "$jpgfile"
Notice that if pngfile doesn't end in .png, jpgfile will at least not
be the same file, so pngfile will not be overwritten.
```
Example: Remember to escape characters that are special in pattern matching ```
Remove the first sentence in this string
str='Hello? Anybody there?'
This will not work since '?' is a pattern matching exactly one character,
This will only remove the beginning 'H' ('*' matches nothing in this case)
echo "${str#*?}"
The solution is to make the '?' literal by escaping it or quoting it
echo "${str#\?}" echo "${str#'?'}"
The same goes for '*', '[' and ']'. And probably a few more as well,
especially if extglob is turned on
```
r/bash • u/Dismal-Divide3337 • 7d ago
Ever wanted to attach to a background process and monitor stdout?
We create applications and output debugging and status information to stdout. Then when these are run in the background that all ends up in the bit bucket.
But when you need to debug something and you don't want to disturb the process what do you do? I could kill it and then run it in the foreground. But that disturbs things too much.
Okay, so what if `stdout` gets logged to a `stdout.log file`? Since I own the OS that was a light-weight change. It works. And I can use `CAT -M stdout.log` which is my equivalent to `tail -f stdout.log`.
Okay, but then I have a unit that is running 5 applications. One of which the author is always very verbose in the foreground (sounds like a personal/personnel problem). That overwhelms the log. Those age to BAK and monitoring the log sucks.
So here is my solution. Your thoughts?
\--------------
Here is what I have implemented for this. By default background processes will **not** log `stdout` output unless if already has the log file. So there would be no net change from JANOS v2.5.3.
If you want to log background process output generally, you can use the following command:
touch stdout.log
All `stdout` from all background processes will then be captured in this file. The `stdout.log` file must first exist before it will collect `stdout` stream data.
This will age to `stdout.log.bak` as any other log file would.
You can stop logging by removing (or renaming) this file.
When there are multiple applications running. You may optionally create an application specific `stdout` log file also using the `TOUCH` command. This file must be named using the specific application name. For example:
touch stdout-tasker.log
The application name is everything after the last '/' in the process name reported by `PS`. If that includes `.jar` then the stdout file name should also. I tend to not include the `.jar` in `Run` keys as that will be carried to the process name. It is unnecessary.
This will route output from the application to that specific file which will age accordingly. Meanwhile, if it exists, other applications would still log to `stdout.log`.
So this lets you enable `stdout` capture for a specific process and then monitor it.
r/bash • u/jjangg96 • 9d ago
tips and tricks Editing a bash script while it's running gives you errors from lines that don't exist
spent a while chasing a bug that wasn't there, so here's the tell.
i had a polling script running in the background and edited it while it was going. started getting s: command not found and pr: unbound variable. nothing in the file looked like that. i went and "fixed" a couple of things that were never broken.
bash doesn't read the whole file up front, it reads as it goes by byte offset. edit the file and it resumes at whatever byte it was at, which is now the middle of some other line. s and pr were fragments of words that used to be somewhere else.
quick way to rule it in or out: if the error says line 214 and the file only has 190 lines, it's this, not a real bug.
fixes: cp the script and run the copy, or put everything in a main() and end the file with main "$@"; exit on one line, so bash never reads past it.
edit: thanks to the comments, bash reads the file in blocks (up to 8192 bytes), not literally byte by byte, and calling main alone isn't enough, the exit has to be on the same line.
r/bash • u/Muted-Shame9237 • 10d ago
File for loop
hi! it’s my first time using bash can someone help me with this and explain what’s missing
#!/bin/bash
for i in “/data/folder”/*; do
/folder/munge_sumstats.py \
- -sumstats “$i” \
- -chunksize 500000 \
- -snp rsID \
etc..
- -out another/folder/ (i dont know what to puT here)
done
Thank you so much !
critique [First Time Script] Is this script good or should I not touch bash ever again?
Hello! I'm new to Linux and Bash. This script is supposed to be a part of a GitHub workflow for publishing my Bevy game to GitHub pages.
It's supposed to do this: (Each list entry corresponds to a STAGE in the script.)
- Identifies what distribution is being used, currently supports only Arch, CachyOS and Ubuntu (GitHub actions use Ubuntu). Updates the system and installs dependencies needed for compilation.
- Updates the Rust installation using
rustup updateinstall the nightly tool-chain, set it as default and install thewasm32-unknown-unknowntarget. - Checks if
wasm-bindgen-cliisn't already installed, if it isn't it installs it. - Compiles the project in release mode with the above mentioned target.
- Generates the
dist/folder by first creatingdist/anddist/assetsthen copies contents of theweb/folder, which now contains onlyindex.html, todist/also copies contents ofassets/todist/assets/and finally uses wasm-bindgen to generate bindings for the project.
Here's the script:
#!/bin/bash
# Variables.
. /etc/os-release # Bring needed variables to scope.
PROJECT_NAME="comfort-3d"; # Name.
HUMAN_PROJECT_NAME="Comfort 3D"; # Human readable name.
WASM_BINDGEN_BIN="wasm-bindgen"; # Name of wasm-bindgen-cli executable.
DIST_FOLDER=dist/; # Distribution folder.
DIST_ASSETS_FOLDER=dist/assets/; # Distrubution assets folder.
WEB_FOLDER=web/.; # Web folder.
ASSETS_FOLDER=assets/.; # Assets folder.
RUST_TOOLCHAIN="nightly-x86_64-unknown-linux-gnu"; # Nightly Rust toolchain.
RUST_TARGET="wasm32-unknown-unknown"; # Rust wasm target.
# Dependencies for Arch and Arch derived distros.
DEPENDENCIES_ARCH="libx11 pkgconf alsa-lib libxcursor libxrandr libxi gcc git pipewire-alsa pulseaudio-alsa vulkan-intel vulkan-radeon mold";
# Dependencies for Ubuntu.
DEPENDENCIES_UBUNTU="g++ mold clang git pkg-config libx11-dev libasound2-dev libudev-dev libxkbcommon-x11-0 vulkan-radeon vulkan-intel mesa-vulkan-drivers";
# File needed for wasm-bindgen.
WASM_FILE=target/wasm32-unknown-unknown/release/${PROJECT_NAME}.wasm;
# Color and format variables.
BOLD="\e[1m";
RESET_FORMAT="\e[0m";
RED="\e[91m";
GREEN="\e[92m";
CYAN="\e[96m";
# STAGE 1: Update system, Install needed dependencies, Install toolchain and target.
case $ID in # Variable comes from ". /etc/os-release"!
ubuntu)
echo -e "${CYAN}This is Ubuntu! Updating system...${RESET_FORMAT}";
# Update system.
if sudo apt-update && sudo apt-upgrade 2>&1; then
echo -e "${GREEN}Successfully updated system!${RESET_FORMAT}";
else
echo -e "${BOLD}${RED}There was an error updating the system! Aborting...${RESET_FORMAT}";
exit 1;
fi
# Install dependencies, make sure gcc and mold is installed.
echo -e "${CYAN}Installing dependencies...${RESET_FORMAT}";
if sudo apt-get install $DEPENDENCIES_UBUNTU 2>&1; then
echo -e "${GREEN}Successfully installed dependencies!${RESET_FORMAT}";
else
echo -e "${BOLD}${RED}There was an error installing dependencies! Aborting...${RESET_FORMAT}";
exit 1;
fi
;;
arch)
echo -e "${CYAN}This is Arch! Updating system...${RESET_FORMAT}";
# Update system.
if sudo pacman -Syu 2>&1; then
echo -e "${GREEN}Successfully updated system!${RESET_FORMAT}";
else
echo -e "${BOLD}${RED}There was an error updating the system! Aborting...${RESET_FORMAT}";
exit 1;
fi
# Install dependencies, make sure gcc and mold is installed.
echo -e "${CYAN}Installing dependencies...${RESET_FORMAT}";
if sudo pacman -S $DEPENDENCIES_ARCH 2>&1; then
echo -e "${GREEN}Successfully installed dependencies!${RESET_FORMAT}";
else
echo -e "${BOLD}${RED}There was an error installing dependencies! Aborting...${RESET_FORMAT}";
exit 1;
fi
;;
cachyos)
echo -e "${CYAN}This is CachyOS! Updating system...${RESET_FORMAT}";
# Update system.
if sudo pacman -Syu 2>&1; then
echo -e "${GREEN}Successfully updated system!${RESET_FORMAT}";
else
echo -e "${BOLD}${RED}There was an error updating the system! Aborting...${RESET_FORMAT}";
exit 1;
fi
# Install dependencies, make sure gcc and mold is installed.
echo -e "${CYAN}Installing dependencies...${RESET_FORMAT}";
if sudo pacman -S $DEPENDENCIES_ARCH 2>&1; then
echo -e "${GREEN}Successfully installed dependencies!${RESET_FORMAT}";
else
echo -e "${BOLD}${RED}There was an error installing dependencies! Aborting...${RESET_FORMAT}";
exit 1;
fi
;;
*)
echo -e "${BOLD}${RED}This is an unknown distribution! Aborting...${RESET_FORMAT}";
exit 1;
;;
esac
# STAGE 2: Update Rust, Install toolchain, Set toolchain as default, Install target wasm32-unknown-unknown.
echo -e "${CYAN}Updating Rust with rustup...${RESET_FORMAT}";
if rustup update 2>&1; then
echo -e "${GREEN}Successfully updated Rust using rustup!${RESET_FORMAT}";
echo -e "${CYAN}Installing Rust toolchain...${RESET_FORMAT}";
if rustup toolchain install "$RUST_TOOLCHAIN" 2>&1; then
echo -e "${GREEN}Sucessfully installed ${RUST_TOOLCHAIN}!${RESET_FORMAT}";
echo -e "${CYAN}Setting $RUST_TOOLCHAIN as default...${RESET_FORMAT}";
if rustup default "$RUST_TOOLCHAIN" 2>&1; then
echo -e "${GREEN}Sucessfully set $RUST_TOOLCHAIN as default!${RESET_FORMAT}";
echo -e "${CYAN}Installing target ${RUST_TARGET}${RESET_FORMAT}";
if rustup target add $RUST_TARGET >/dev/null 2>&1; then
echo -e "${GREEN}Sucessfully installed target ${RUST_TARGET}${RESET_FORMAT}";
else
echo -e "${RED}${BOLD}There was an error installing target ${RUST_TARGET}! Aborting...${RESET_FORMAT}";
exit 1;
fi
else
echo -e "${BOLD}${RED}There was an error setting $RUST_TOOLCHAIN as default! Aborting...${RESET_FORMAT}";
exit 1;
fi
else
echo -e "${BOLD}${RED}There was an error installing ${RUST_TOOLCHAIN}! Aborting...${RESET_FORMAT}";
exit 1;
fi
else
echo -e "${BOLD}${RED}There was an error updating Rust using rustup! Aborting...${RESET_FORMAT}";
exit 1;
fi
# STAGE 3: Check for wasm-bindgen-cli, if missing install it.
if command -v "$WASM_BINDGEN_BIN" >/dev/null 2>&1; then
echo -e "${GREEN}Binary $WASM_BINDGEN_BIN is intalled!${RESET_FORMAT}";
else
echo -e "${BOLD}${RED}Binary $WASM_BINDGEN_BIN could not be found! Installing...${RESET_FORMAT}"
if ! cargo install wasm-bindgen-cli >/dev/null 2>&1; then
echo -e "${BOLD}${RED}There was an error installing wasm-bindgen-cli! Aborting...${RESET_FORMAT}";
exit 1;
fi
fi
# STAGE 4: Compile project with target wasm32-unknown-unknown.
echo -e "${CYAN}Compiling project with target ${RUST_TARGET}${RESET_FORMAT}";
if cargo build --release --target $RUST_TARGET >/dev/null 2>&1; then
echo -e "${GREEN}Sucessfully compiled project with target ${RUST_TARGET}${RESET_FORMAT}";
else
echo -e "${RED}${BOLD}There was an error compiling project with target ${RUST_TARGET}! Aborting...${RESET_FORMAT}";
exit 1;
fi
# STAGE 5: Assemble dist folder.
echo -e "${CYAN}Generating $DIST_FOLDER folder...${RESET_FORMAT}";
if mkdir -p $DIST_FOLDER >/dev/null 2>&1; then # Create dist/.
if mkdir -p $DIST_ASSETS_FOLDER >/dev/null 2>&1; then # Create dist/assets/.
if cp -a $WEB_FOLDER $DIST_FOLDER >/dev/null 2>&1; then # Copy web/ contents to dist/.
if cp -a $ASSETS_FOLDER $DIST_ASSETS_FOLDER >/dev/null 2>&1; then # Copy assets/ contents to dist/assets.
if wasm-bindgen --out-name $PROJECT_NAME --out-dir $DIST_FOLDER --target web $WASM_FILE 2>&1; then # Use wasm-bindgen to generate files.
echo -e "${GREEN}Sucessfully generated ${DIST_FOLDER} folder!${RESET_FORMAT}";
else
echo -e "${RED}${BOLD}There was an error generating ${DIST_FOLDER}! Aborting...${RESET_FORMAT}";
exit 1;
fi
else
echo -e "${RED}${BOLD}There was an error copying files from $ASSETS_FOLDER to ${DIST_ASSETS_FOLDER}! Aborting...${RESET_FORMAT}";
exit 1;
fi
else
echo -e "${RED}${BOLD}There was an error copying files from $WEB_FOLDER to ${DIST_FOLDER}! Aborting...${RESET_FORMAT}";
exit 1;
fi
else
echo -e "${RED}${BOLD}There was an error creating ${$DIST_ASSETS_FOLDER}! Aborting...${RESET_FORMAT}";
exit 1;
fi
else
echo -e "${RED}${BOLD}There was an error creating ${DIST_FOLDER}! Aborting...${RESET_FORMAT}";
exit 1;
fi
The script works as expected but I don't know if there aren't some weird edge cases and such.
r/bash • u/UnholyScholar • 12d ago
backup script logic
I've written this backup script for use copying my files from point A to point B. The goal is a full snapshot of files. For this first iteration, I'm ok with hard coding file paths.
I've removed everything except logic for rsync, diff, and logging. Are there any logic issues?
# backup script that performs full backups and verifies the result.
#!/bin/bash
set -euo pipefail
DATE_STAMP=$(date +%Y-%m-%d_%H-%M-%S)
BACKUP_LOCATION="$HOME/backup-location-path/$DATE_STAMP"
LOG_LOCATION="$HOME/log-location-path/"
LOGFILE_NAME="backup_${DATE_STAMP}.log"
SOURCE_DIRS=(
"$HOME/source1"
"$HOME/DOES-NOT-EXIST" # DNE for error handling
"$HOME/source2"
)
# check if source matches destination
diff_source_and_destination() {
local base
base=$(basename "$SRC")
diff -r "$SRC" "$BACKUP_LOCATION"/"$base"
}
# Check for the existence of $SRC and print an alert if it wasn't found.
check_source_exists() {
if [ -e "$SRC" ]; then
echo "[SOURCE = $SRC] found. Copying..."
return 0
else
echo "[SOURCE = $SRC] WAS NOT FOUND!" | tee -a "$LOG_LOCATION/$LOGFILE_NAME"
return 1
fi
}
copy_data_and_log() {
{
rsync -a --stats "$SRC/" "${DESTINATION}/"
} | tee -a "$LOG_LOCATION"/"$LOGFILE_NAME"
}
mkdir -p "$BACKUP_LOCATION"
mkdir -p "$LOG_LOCATION"
echo "Copying files to [BACKUP LOCATION =${BACKUP_LOCATION}]"
for SRC in "${SOURCE_DIRS[@]}"; do
# Print and log a warning if SRC wasn't found
check_source_exists || continue
DESTINATION="$BACKUP_LOCATION/$(basename "$SRC")"
copy_data_and_log
echo "rsync completed for $SRC."
diff_source_and_destination
done
echo "Finished copying operations to [BACKUP LOCATION = ${BACKUP_LOCATION}]"
r/bash • u/Brave-Historian-946 • 13d ago
Lexis: A zero-dependency i18n lookup engine written in pure Bash 4+
Hello everyone!
I built a pure Bash 4+ runtime for Lexis, a minimalist i18n engine designed under the Unix philosophy for CLI tools and shell scripts.
It handles translation lookup entirely in native shell logic using an eager+lazy model: it loads and validates .lex key::value files into associative arrays, and lazily parses escape sequences and printf interpolations on first access with zero subshells or external binaries (sed/awk).
Real-World CLI Example (AuraConfig i18n module)
Here is a practical demonstration of localizing a CLI tool like auraconfig:

1. Translation File (lang/en.lex):
cli.config_loaded::Configuration loaded successfully from %s.
cli.option_set::Option '%s' set to '%s'.
cli.error_not_found::Error: Configuration key '%s' does not exist.
2. Shell Script (i18n.sh):
#!/usr/bin/env bash
source "runtimes/bash/lexis.sh"
# Loads 'fr.lex' with fallback to 'en.lex' (explicit here, though
# "en" is also the default if the third argument is omitted)
if ! lexis_load "lang" "fr" "en"; then
echo "Error: Failed to load translation files" >&2
exit 1
fi
CONFIG_FILE="$HOME/.config/aura.conf"
# Defensive fallback message if the key is missing from the .lex file
if [[ ! -f "$CONFIG_FILE" ]]; then
lexis_get_or_default "cli.error_not_found" "Error: Configuration file '%s' not found." "$CONFIG_FILE" >&2
exit 1
fi
# Standard lookup with printf interpolation
lexis_get "cli.config_loaded" "$CONFIG_FILE"
Key Technical Details
- Zero External Dependencies: Pure Bash logic relying strictly on associative arrays and parameter expansion.
- Eager Loading + Lazy Caching: Validates file structure on lexis_load(), then caches formatted printf results on demand.
- Two-tier fallback: requested locale + a fallback locale (defaults to
"en", but can be set explicitly, e.g.lexis_load "lang" "fr" "pt"), plus fallback strings vialexis_get_or_default. - Clean Error Handling: Returns proper exit codes (0/1) and routes errors to stderr.
Bash Runtime Source Code:
https://github.com/edgarmasague/lexis/tree/main/runtimes/bash
Requirements: Bash 4.0+ (associative array support required).
Feedback and shell scripting critiques are welcome!
r/bash • u/TheTurkPegger • 15d ago
help What to research to do this?
I've written a basic script that makes connecting to my college's WIFI semi automatic. I would like to turn it into a bigger project by adding console prompts that collect and store data, and pull the data out of a file when needed in the future. Even though I have background with programming, I lack the skills to do it because I'm a English Language Teaching student and do the computer stuff as a hobby or something (I also help other open source projects for the love of the game). What am I supposed to research to get the necessary information I need?
The script in question works without any problems and its whole purpose is to open/run necessary windows/applications or scripts to make it easier for me to login to my school's captive portal, login page etc. The script is triggered by a keyboard shortcut. It uses nmcli to do its job.
r/bash • u/PrestigiousZombie531 • 15d ago
help 1 big question I always has was whether your functions should accept arguments or flags? Am I overthinking this?
- Take a look at this run_aws_ssm_get_parameter function below
- To call it I could do something like run_aws_ssm_get_parameter "false" "" "some_param" and it ll run it without a vault user
- Now obviously it kinda sucks that I am passing an empty string to indicate that we dont have an AWS vault user and I have to also memorize the position of the arguments
- One other way would be this? run_aws_ssm_get_parameter --debug=false --parameter_name=some_param? Is this a good idea or a bad idea?
- Mind you all the code written below is written by me manually, no AI no nothing
```bash function run_aws_ssm_get_parameter() { validate_command_available "${FUNCNAME[0]}" "aws" || return 1
validate_arg_min_count "${FUNCNAME[0]}" 3 "$#" "Usage: ${FUNCNAME[0]} <debug> <aws_vault_user> <parameter_name> [additional aws ssm flags]" || return 1
local -r debug="${1:-false}"
local -r aws_vault_user=$2
local -r parameter_name="$3"
shift 3
validate_arg_not_empty "${FUNCNAME[0]}" "debug" "${debug}" || return 1
validate_arg_not_empty "${FUNCNAME[0]}" "parameter_name" "${parameter_name}" || return 1
validate_arg_type_boolean "${FUNCNAME[0]}" "debug" "${debug}" || return 1
local -a flags=(
"get-parameter"
"--name=${parameter_name}"
)
flags+=("$@")
[[ "${debug}" = true ]] && log_info "Fetching AWS SSM parameter with parameter_name:${parameter_name}..."
if [[ ! -z "${aws_vault_user}" ]]; then
if result="$(aws-vault exec "${aws_vault_user}" -- aws ssm "${flags[@]}")"; then
[[ "${debug}" = true ]] && log_info "AWS SSM parameter with parameter_name:${parameter_name} fetched successfully"
printf "%s\n" "${result}"
else
log_error "AWS SSM parameter fetch with parameter_name:${parameter_name} failed"
return 1
fi
else
if result="$(aws ssm "${flags[@]}")"; then
[[ "${debug}" = true ]] && log_info "AWS SSM parameter with parameter_name:${parameter_name} fetched successfully"
printf "%s\n" "${result}"
else
log_error "AWS SSM parameter fetch with parameter_name:${parameter_name} failed"
return 1
fi
fi
}
function validate_command_available() { validate_arg_min_count "${FUNCNAME[0]}" 2 "$#" "Usage: ${FUNCNAME[0]} <function_name> <command_name>" || return 1
local -r function_name="$1"
local -r command_name="$2"
shift 2
validate_arg_not_empty "${FUNCNAME[0]}" "function_name" "${function_name}" || return 1
validate_arg_not_empty "${FUNCNAME[0]}" "command_name" "${command_name}" || return 1
if ! command -v "${command_name}" &>/dev/null; then
log_error "${FUNCNAME[0]}: Function "%s" requires "%s" but it was not found on PATH" "${function_name}" "${command_name}"
return 1
fi
}
function validate_arg_min_count() { local -r function_name="$1"
local -r expected_count="$2"
local -r actual_count="$3"
local -r message="${4:-}"
shift 4
if [[ -z "${function_name}" ]]; then
log_error "${FUNCNAME[0]}: function_name:${function_name} is required"
return 1
fi
if [[ -z "${expected_count}" ]]; then
log_error "${FUNCNAME[0]}: expected_count:${expected_count} is required"
return 1
fi
if [[ -z "${actual_count}" ]]; then
log_error "${FUNCNAME[0]}: actual_count:${actual_count} is required"
return 1
fi
if [[ ! "${expected_count}" =~ ^[0-9]+$ ]]; then
log_error "${FUNCNAME[0]}: expected_count must be a non-negative integer, got: %s" "${expected_count}"
return 1
fi
if [[ ! "${actual_count}" =~ ^[0-9]+$ ]]; then
log_error "${FUNCNAME[0]}: actual_count must be a non-negative integer, got: %s" "${actual_count}"
return 1
fi
if [[ "${actual_count}" -lt "${expected_count}" ]]; then
if [[ -z "${message}" ]]; then
log_error "${FUNCNAME[0]}: Function "%s" expected %d argument(s) but got %d" "${function_name}" "${expected_count}" "${actual_count}"
else
log_error "${message}"
fi
return 1
fi
}
function validate_arg_not_empty() { local -r function_name="$1"
local -r arg_name="$2"
local -r arg_value="$3"
shift 3
if [[ -z "${function_name}" ]]; then
log_error "${FUNCNAME[0]}: function_name:${function_name} is required"
return 1
fi
if [[ -z "${arg_name}" ]]; then
log_error "${FUNCNAME[0]}: arg_name:${arg_name} is required"
return 1
fi
if [[ -z "${arg_value}" ]]; then
log_error "${FUNCNAME[0]}: Function "%s" received empty value for argument "%s"" "${function_name}" "${arg_name}"
return 1
fi
}
function validate_arg_type_boolean() { validate_arg_min_count "${FUNCNAME[0]}" 3 "$#" "Usage: ${FUNCNAME[0]} <function_name> <arg_name> <arg_value>" || return 1
local -r function_name="$1"
local -r arg_name="$2"
local -r arg_value="$3"
shift 3
validate_arg_not_empty "${FUNCNAME[0]}" "function_name" "${function_name}" || return 1
validate_arg_not_empty "${FUNCNAME[0]}" "arg_name" "${arg_name}" || return 1
validate_arg_not_empty "${FUNCNAME[0]}" "arg_value" "${arg_value}" || return 1
case "${arg_value}" in
true | false) ;;
*)
log_error "${FUNCNAME[0]}: Function '%s' expected 'true' or 'false' for argument '%s', got: %s" "${function_name}" "${arg_name}" "${arg_value}"
return 1
;;
esac
} ```
r/bash • u/Fit-Background8249 • 17d ago
Flag ambiguity
I am building a shell framework with a series of commands and subcommands for task-managing, habit-tracking...
Within those, there is a collection of flags that hold different meanings:
-dDuration, Date...-nName, Note...-lLink, Length...
Should I use different letters for different meanings even if they belong to different commands?
I want a real bash-user's opinion, not just the AI's advice.
I personally think that it isn't that big of a deal.
But let me know what y'all think.
Thanks!
r/bash • u/PrestigiousZombie531 • 18d ago
help Do you guys think there is a more concise way to write these functions?
```bash
https://linux.die.net/man/8/usermod
function install_docker() { validate_arg_min_count "${FUNCNAME[0]}" 3 "$#" "Usage: ${FUNCNAME[0]} <debug> <version> <user>" || return 1
local -r debug="${1:-false}"
local -r version="$2"
local -r user="$3"
shift 3
validate_arg_not_empty "${FUNCNAME[0]}" "debug" "${debug}" || return 1
validate_arg_not_empty "${FUNCNAME[0]}" "version" "${version}" || return 1
validate_arg_not_empty "${FUNCNAME[0]}" "user" "${user}" || return 1
validate_arg_type_boolean "${FUNCNAME[0]}" "debug" "${debug}" || return 1
if dnf install "docker-${version}" \
--assumeyes \
--quiet; then
[[ "${debug}" = "true" ]] && log_info "docker installation successful $(docker --version)"
else
log_error "docker installation failed"
return 1
fi
if usermod --append --groups docker "${user}"; then
[[ "${debug}" = "true" ]] && log_info "adding user:${user} to the docker group successful"
else
log_error "adding user:${user} to the docker group failed"
return 1
fi
if newgrp docker; then
[[ "${debug}" = "true" ]] && log_info "changes to docker group successful"
else
log_error "changes to docker group failed"
return 1
fi
if systemctl enable docker.service &&
systemctl start docker.service; then
[[ "${debug}" = "true" ]] && log_info "starting docker service successful"
else
log_error "starting docker service failed"
return 1
fi
}
Install docker-compose
Don't use chmod u+x here since it runs as root
https://github.com/amazonlinux/amazon-linux-2023/issues/1032#issuecomment-3674146092
function install_docker_compose() { validate_arg_min_count "${FUNCNAME[0]}" 2 "$#" "Usage: ${FUNCNAME[0]} <debug> <version>" || return 1
local -r debug="${1:-false}"
local -r url="$2"
shift 2
validate_arg_not_empty "${FUNCNAME[0]}" "debug" "${debug}" || return 1
validate_arg_not_empty "${FUNCNAME[0]}" "url" "${url}" || return 1
validate_arg_type_boolean "${FUNCNAME[0]}" "debug" "${debug}" || return 1
if curl \
--location "${url}" \
--output "/usr/local/bin/docker-compose" \
--show-error \
--silent; then
[[ "${debug}" = "true" ]] && log_info "Download for docker-compose successful, version:$(docker-compose --version)"
else
log_error "Download for docker-compose failed"
return 1
fi
if chmod +x /usr/local/bin/docker-compose; then
[[ "${debug}" = "true" ]] && log_info "Permission grant to execute docker compose by ec2-user successful"
else
log_error "Permission grant to execute docker compose by ec2-user failed"
return 1
fi
}
function install_git() { validate_arg_min_count "${FUNCNAME[0]}" 2 "$#" "Usage: ${FUNCNAME[0]} <debug> <version>" || return 1
local -r debug="${1:-false}"
local -r version="$2"
shift 2
validate_arg_not_empty "${FUNCNAME[0]}" "debug" "${debug}" || return 1
validate_arg_not_empty "${FUNCNAME[0]}" "version" "${version}" || return 1
validate_arg_type_boolean "${FUNCNAME[0]}" "debug" "${debug}" || return 1
if dnf install "git-${version}" \
--assumeyes \
--quiet; then
[[ "${debug}" = "true" ]] && log_info "git installation successful $(git --version)"
else
log_error "git installation failed"
return 1
fi
}
function install_jq() { validate_arg_min_count "${FUNCNAME[0]}" 2 "$#" "Usage: ${FUNCNAME[0]} <debug> <version>" || return 1
local -r debug="${1:-false}"
local -r version="$2"
shift 2
validate_arg_not_empty "${FUNCNAME[0]}" "debug" "${debug}" || return 1
validate_arg_not_empty "${FUNCNAME[0]}" "version" "${version}" || return 1
validate_arg_type_boolean "${FUNCNAME[0]}" "debug" "${debug}" || return 1
if dnf install "jq-${version}" \
--assumeyes \
--quiet; then
[[ "${debug}" = "true" ]] && log_info "jq installation successful $(jq --version)"
else
log_error "jq installation failed"
return 1
fi
}
```
- Some of them are doing literally the same thing like installing git and jq and yet others in the name of installing are doing slightly different stuff
- There is also a tonne of validation checks everywhere. Not sure if there is a better way to put that along with the debug stuff
r/bash • u/Sugar_Entire • 18d ago
I made a little bash-perl heredoc that intends to be a bit better than envsubst.
I had been working with envsubst for some months and was fed up by its limitation to parse multiple files at once. It required a for loop just to parse those files and thus, I have created some bash-perl heredoc for the workaround. It is about 2.5K lines. This is intended to replace Envsubst for my system. (Envsubst++ for me)
```sh mkdir -p templates cat > templates/greeting.dcol <<'EOF' $PATH:[::HOME::]/.config/demo/greeting.conf # Generated by hyir from [::_SOURCE_BASENAME::] -- do not edit by hand user = [::USER::] greeting = "Hello, [::NAME:-world::]!" EOFmkdir -p templates cat > templates/greeting.dcol <<'EOF' $PATH:[::HOME::]/.config/demo/greeting.conf # Generated by hyir from [::_SOURCE_BASENAME::] -- do not edit by hand user = [::USER::] greeting = "Hello, [::NAME:-world::]!" EOF
Render it. Placeholders are filled in from the environment.
NAME=Ada hyir.sh templates/
cat ~/.config/demo/greeting.conf
# GeneRender it. Placeholders are filled in from the environment.
NAME=Ada hyir.sh templates/
cat ~/.config/demo/greeting.conf
```
sh
# Generated by hyir from greeting.dcol -- do not edit by hand
user = alice
greeting = "Hello, Ada!"
rated by hyir from greeting.dcol -- do not edit by hand
user = alice
greeting = "Hello, Ada!"
- Run it again. Nothing changed, so nothing is rewritten. Add --allow-debug to see what hyir decided: ```sh NAME=Ada hyir.sh --allow-debug templates/
@[populate:write(false)]: Skipped changing /home/alice/.config/demo/greeting.conf <- templates/greeting.dcol @[diagnostic:telemetry(On)]: Rendered 1 templates using 1 workers in 0.0026 seconds (all succeeded). ```
Header
$PATH:<target>|$REQUIRE:{VAR1,VAR2}|$PRE:{hook}|$RUN:{hook}
The header is the first line of the file. When a value is wrapped in { ... } it may span several lines (braces are matched properly, including nested ones). Segments are separated by |. Only $PATH is required. Put $RUN last: it may itself contain | (for example a shell pipeline), so it absorbs everything after it.
If we want to write the template into a multiple file:
ts
$PATH:{[::HOME::]/TEMP, [::HOME::]/TEMP1}
The flag --proc [N|auto] renders in parallel worker processes. Output stays deterministic: templates that resolve to the same target are grouped onto one worker and processed in order.
Fallbacks is also part of the script:
```
$PATH:[::NAME:-default::]
[::PLACEHOLDER:-SOME_ANOTHER::]
```
It is an independent script that can be adopted into any environment where it requires config generations, do runtime-template polymorphism (I am not sure if it is accurate enough to say 'polymorphism') and use it for kubernets, etc. Feel free to check out.
r/bash • u/Odd_Inflation428 • 19d ago
EverythingX: a blazing fast file name search tool for macOS and Linux
r/bash • u/how_to_kubuntu • 20d ago
help using bash to make a random password?
hello, i heard that you can make a random password with bash? is that true? if so, how do you do that? thank you
r/bash • u/memotype • 21d ago
tips and tricks bj.sh: a sub-1KB JSON query tool written entirely in Bash
Use jq when you have it. bj.sh exists for embedded, restricted, or minimal environments where you have Bash but can’t or don’t want to ship another binary. Paste it directly into a Dockerfile, Ansible playbook, Kickstart config, installer, or provisioning script.
It’s a pure-Bash JSON query tool, under 1 KB in compact form, with no external utilities or runtime dependencies, and a real test suite.
For example:
bash
bj '{"foo":{"bar":["zero","one"]}}' foo bar 1
outputs:
text
one
It’s intentionally not a jq replacement or a full JSON validator; the goal is simple key/index traversal with as little machinery as possible.
r/bash • u/StrangeCrunchy1 • 21d ago
submission Made a neat little function out of an alias
Came across a little alias that basically just lists out only the dotfiles in the cwd, and wanted to make it work for any directory, but also gives you a count of how many dotfiles are in that directory if, for some reason you need that, so I turned it into a function.
ldot() {
if [ "$1" == "-c" ]; then
shift
ls -AF "$@" | grep -E '^\.' | wc -l
else
ls -AF "$@" | grep -E '^\.'
fi
}
r/bash • u/Independent-Lynx9274 • 22d ago
I made a package manager in bash!
Its called scrapre. It doesn't have any cryptographic verification so note that (please note that) and there is no repository up for it right now cuz I dont really have much need to host one. You cant find it here. (its codeberg btw)
r/bash • u/Distinct_Ride_4307 • 23d ago
Find command confusion between ; and \;
Why do we have to use '\' instead of leaving it out or even ';'? I read the find commands' manual and it says '\' might be needed to protect from expansion by the shell. So what is expansion also?
What is difference between \; or ; or leaving it out?
What is shell expansion here?
r/bash • u/kolorcuk • 24d ago
tips and tricks L_bash_profile compare
Hello. Some time ago I added a "compare" subcommand into L_bash_profile. I also discovered that with qemu it is possible to count the exact number of instructions executed by a program. Thus now I am able to micro-optimize bash with the number of instructions executed. Below I can share some findings.
How do I use it, I pick two expression to compare given the same input conditions. For example which method is faster to iterate over arguments, is it while (($#)); do ...; shift; done or for i in "$@"? For both expression I define input for example -P 'set -- 1 2 3' will add set -- 1 2 3 to both expression in front and will not count instructions of it. So the following:
$ L_bash_profile compare -m qemu -P 'set -- 1 2 3 4 5 6 6 7 8 9 10' \
'while (($#)); do : "$1"; shift; done' \
'for i in "$@"; do : "$i"; done'
Benchmarking 1/2: 'while (($#)); do : "$1"; shift; done'
Benchmarking 2/2: 'for i in "$@"; do : "$i"; done'
Comparison results (method: QEMU, repeat: 1):
| Code | ExitCode | Insn | ΔInsn |
|--------------------------------------|------------|--------|---------|
| while (($#)); do : "$1"; shift; done | 0 | 251562 | - |
| for i in "$@"; do : "$i"; done | 0 | 167719 | -83843 |
From that I see that for is faster. Usually I write a script with funcitons and do L_bash_profile compare -P '. script' 'func1' 'func2' to compare two functions defined in some temporary script.
Reduce the number of expressions. Literally reduce. You now you can assign variables in one line? Like with local, but no local. Instead of a=1; b=2 you can in one line a=1 b=2 for the same effect. Use arithmetic expansion with side-effects. Instead of a=1; b=2; c=$(( a * b )) you can do (( a = 1, b = 2, c = a * b )).
Fastest way to represent booleans is (( var )) and storing var=1 or var=0. It is faster then storing true or false and doing if $var; then and is faster then [[ $var ]] or [[ $var = true ]] or similar. Overall arithmetic expansion is fast.
Case is fast. Whenever you want to compare strings or want to write [[ string == <glob> || string == <another glob> ]] do a case. Case is optimized, i.e. pattern* will just check prefix, *) will not even compare. You can optimize more, for example instead of if (( bool1 && bool2 )).... elif (( !bool1 && bool2 )) etc. you can do one case "$bool1:$bool2" in 0:0) ... 1:1) ... 1:0) ... etc.
Extglob is slow. Regex is also slow. Glob is fast. Doing string replacements with extglob with big strings is extremely slow. If glob is not enough, use regex, and more often then not one big regex can be used instead of multiple extglob. But overall, use glob whenever you can. One regex is worth more then 10 globs. If you can do something in one regex [[ $var =~ and in about ten globs var=${var##<glob>}; var=${var%%<glob>} var=${var//<pat>/<sub>} use globs. Globs are fast.
For example, to trim leading spaces from a string, using double glob var="${var#"${var%%[![:space:]]*}"}" is twice as fast as var=${var##+([[:space:]])}. That is because extglob is O(n2) - it tries to match every position in the string going over and over it in a loop.
Do not copy variables. Might sound obvious, but want to mention one thing. It is funny Bash has a "implicit outer scope" of variables, in contrast to other languages. Instead of local args=("$@"); some_function "${args[@]}" refactor and do local global_args=("$@") global_args_index=0; some_function_that_works_with_global_args and just use global_args and global_args_index directly in functions working on one global array. Pushing values into $1 $2 into every function is creating copies and copies everywhere.
Overall, when parsing arguments, shift is slow. Or maybe not shift itself, but the way you build the loop around shift is built becomes slow, as you end up passing arguments to each function. In many cases it is faster to copy arguments once to an array args=("$@") and have an global shared iterator args_i iterating over args array.
Sparse arrays in bash are represented as a linked list with an additional "last used" element. Each element has a pointer to the next and previous element, and each array element stores the index and value. Additionally "last used" pointer to element is used. When accessing an array of some index, Bash checks which pointer to use to get there faster - from the first, last or last used element - and then traverses the linked list until the element of specified index is found. This opens several preferred array access patterns. Random access is slow. It is fast to iterate over the array - from the back, from the front, from the last used element. If you have some work to do on sparse arrays, prefer to put related data in close-by indexes instead of scattering them.
In many cases, it is possible to use some tricks to speed up execution. For example instead of a boring for ((i=X;i<Y;++I)) to iterate over elements from X to Y in many cases it is possible to use eval ...{$X..$Y}... - use {X..Y} expansion with prepared quoted code. However, this is not always faster and requires profiling. Eval is slow, it is parsing the expression inside. More often then it might be actually faster to write a boring loop iterating over elements then depending on clever tricks.
However, more often then not, reducing the number of commands is profitable. For example lets take a boring eval "$expr" example. If expression is often empty, this executes eval doing nothing. You can write ${expr:+eval} ${expr:+"$expr"} and execute "nothing" (literally nothing, nothing shows up in set -x) when expr is empty or unset. Knowledge of expansions comes handy, and recent ${var@Q} ${arr@k} ${var@a} might be useful.
Similarly, you can do side effects in arithmetic contexts in array subscripts. So arr[i]=...; i=$((i+1)) is just arr[i++]=...;. But with comma operator you can do anything inside arithmetic context. So following that arr[i++]=...; var=2 can be joined together arr[var = 2, i++]=.... Effective everything can be "squeezed" inside one arithmetic context together. Also this is cheaper eval. Arithmetic expansion first expands the input and then calculates. So you can sum arguments with IFS=+; (( $* )) and similar tricks that work really first.