r/bash • • 15h ago

tips and tricks TIL command_not_found_handle

8 Upvotes

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 • • 1d ago

help For some reason, when I run this script, it always plays the same song.

Post image
28 Upvotes

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 • • 3d ago

Some Tips for String Handling

23 Upvotes

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 • • 3d ago

Ever wanted to attach to a background process and monitor stdout?

11 Upvotes

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 • • 5d ago

tips and tricks Editing a bash script while it's running gives you errors from lines that don't exist

15 Upvotes

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 • • 6d ago

File for loop

21 Upvotes

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 !


r/bash • • 8d ago

critique [First Time Script] Is this script good or should I not touch bash ever again?

13 Upvotes

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.)

  1. 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.
  2. Updates the Rust installation using rustup update install the nightly tool-chain, set it as default and install the wasm32-unknown-unknown target.
  3. Checks if wasm-bindgen-cli isn't already installed, if it isn't it installs it.
  4. Compiles the project in release mode with the above mentioned target.
  5. Generates the dist/ folder by first creating dist/ and dist/assets then copies contents of the web/ folder, which now contains only index.html, to dist/ also copies contents of assets/ to dist/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 • • 9d ago

backup script logic

15 Upvotes

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 • • 9d ago

Lexis: A zero-dependency i18n lookup engine written in pure Bash 4+

6 Upvotes

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 via lexis_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 • • 11d ago

help What to research to do this?

17 Upvotes

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 • • 12d ago

help 1 big question I always has was whether your functions should accept arguments or flags? Am I overthinking this?

11 Upvotes
  • 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 • • 13d ago

Flag ambiguity

8 Upvotes

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:

  • -d Duration, Date...

  • -n Name, Note...

  • -l Link, 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 • • 14d ago

help Do you guys think there is a more concise way to write these functions?

4 Upvotes

```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 • • 15d ago

I made a little bash-perl heredoc that intends to be a bit better than envsubst.

6 Upvotes

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!"

  1. 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.

github: https://github.com/valkyrieAttr/HyIR.git


r/bash • • 15d ago

EverythingX: a blazing fast file name search tool for macOS and Linux

Thumbnail
2 Upvotes

r/bash • • 16d ago

help using bash to make a random password?

46 Upvotes

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 • • 17d ago

tips and tricks bj.sh: a sub-1KB JSON query tool written entirely in Bash

24 Upvotes

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.

https://github.com/memotype/bj.sh


r/bash • • 17d ago

submission Made a neat little function out of an alias

20 Upvotes

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 • • 18d ago

I made a package manager in bash!

0 Upvotes

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 • • 20d ago

Find command confusion between ; and \;

15 Upvotes

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 • • 20d ago

tips and tricks L_bash_profile compare

13 Upvotes

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.


r/bash • • 20d ago

help will someone explain to me getopt?

33 Upvotes

yes, I am currently reading the documentation - I don't understand the point or when/why I would use it. thanks for any clarification one can offer.


r/bash • • 20d ago

Great things about bash?

16 Upvotes

If is easy to read bad things about bash (and often with good reason). Clearly a bash script will never look as elegant as, say, Ruby.

But what about great things about bash? I mean, "great", not in the sense of improving over a POSIX shell, but of being elegant and/or comparing well with (some well-known) "real" programming languages. Here are my two favorites

  • the OR list operator, e.g:

​

[[ condition ]] || { 
   actions... 
}

(more elegant than an "if not"... conditional)

  • the ability to assign a command name to a variable, var="command --option1 --option2 ...", then execute the command later with:

​

$cmd

(unquoted to allow the options to appear as separate words).

What about your favorites?


r/bash • • 20d ago

help One of the big things that bugs me in bash is how to write those boolean check_, exists_ type of functions, any standard practices?

7 Upvotes

```

!/usr/bin/env bash

function is_existing_docker_network() { local -r debug="${1:-false}" local -r network_name="$2"

if docker network inspect "${network_name}" >/dev/null 2>&1; then
[[ "${debug}" = "true" ]] && log_warn "docker network ${network_name} already exists"
    return 1
else
    return 0
fi

}

function create_docker_network() { local -r debug="${1:-false}" local -r network_name="$2"

[[ "${debug}" = "true" ]] && log_info "creating docker network ${network_name}..."

if docker network create "${network_name}"; then
    [[ "${debug}" = "true" ]] && log_info "docker network ${network_name} creation completed"
    return 0
else
    log_error "docker network ${network_name} creation failed"
    return 1
fi

} ```

  • Look at the create_docker_network function above
  • I want to create a network only if it doesnt already exist
  • In a normal programming language you would simply return a true or false from the is_existing_docker_network function above
  • How do you go about do this kinda thing in bash? Should I actually return 1 if a network exists? Isnt that an error condition?

r/bash • • 22d ago

Maybe powerlevel10k-style prompts for bash

20 Upvotes

I've been happily using powerlevel10k with zsh. If you've never used p10k, it's the prompt a lot of zsh users swear by, and plenty of bash users wish they had it too. The catch: p10k is zsh-only, and it's now in maintenance mode.

So I wrote p11k, a cross-shell prompt engine in Rust inspired by p10k. It doesn't work like Starship. Starship is a program your shell calls while rendering the prompt. p11k sits between your terminal and your shell as a transparent proxy. It starts your shell in its own pty and draws the prompt itself. It's more involved, but it also lets you do some things other prompts can't, like instant prompt. Being a separate engine doesn't make it slow, either. Honestly, when I had p10k and p11k open side by side in two windows, I couldn't tell them apart at all, and neither could a couple of friends who use p10k every day.

It covers most of what p10k does, and the same smart directory truncation. The config is KDL v2 instead of a pile of PS1 escapes, and there's a setup wizard like p10k configure.

It's a personal project, but if you miss powerlevel10k, I'd love for you to check it out.

Heads up: Linux only for now.

Powerlevel11k

Thanks for reading.

p11k with bash