Lichendust

I'm Harley, an artist, animator and programmer.
I make all kinds of useless stuff.

Bourne Again Shell

A bunch of guff and notes about Bash because it's nightmarish and if I don't write it down I'll forget
CONTENTS

WSL

Scripts that run seamlessly on WSL

Sometimes you want a script that runs anywhere on any system. In my case, this includes WSL. WSL addresses Windows programs and utilities with .exe as part of the command name as a means of clearly distinguishing them, and while there are some utilities that deal with pathing translation — (wslpath -w /mnt/c/Users/) — there aren't for executables. This is obviously for good reason: you don't want your system, especially one designed by Microsoft, guessing which executable it should substitute. Getting it wrong could be catastrophic for many different reasons.

However, on an individual script basis, this is a fairly simple problem to solve: all you really need to do is detect whether you're running in WSL and generate an alias before executing the remainder of the script. Easier said than done, for some reason, because actual alias aliases don't work in non-interactive shells and detecting WSL is genuinely difficult to do reliably (it's impossible to cover all false-negative cases), as espoused in detail here.

Anyone compiling their own kernels or using a bleeding edge nightly build or something will find uname -a to be unreliable.

I settled on an even simpler check, which was reading the WSL_DISTRO_NAME, an environment variable that's highly unlikely (but still not impossible) to have been set on a non-Windows system. I then spawn a shadowy function that forwards all arguments to the new command —

if [[ ! -z $WSL_DISTRO_NAME ]]
then
    aseprite() {
        aseprite.exe "$@"
    }
fi

This doesn't account for any pathing, but realistically, if you're using WSL at all to call Windows utilities, you're using relative paths anyway.

For a really complex script that was doing a tonne of work, I'd probably use a several-step check, cross-referencing the uname with WSL_DISTRO_NAME and whether any WSL-specific mount points and /etc paths exist to make a sound decision. You can also, don't forget, just ask the user: 'Hey I think you're on WSL, can you confirm this so I don't yeet your system? [N/y]'

Reference

A non-trivial chunk of this section is adapted from the incredible but now unmaintained Pure Bash Bible (MIT License). Some sections are my own, some are rehosting PBB and some are a mix of the two.

Substitutions and Variable Handling

Replacement

ParameterResult
${var#pattern}Remove shortest match of pattern from start of string.
${var##pattern}Remove longest match of pattern from start of string.
${var%pattern}Remove shortest match of pattern from end of string.
${var%%pattern}Remove longest match of pattern from end of string.
${var/pattern/replace}Replace first match with string.
${var//pattern/replace}Replace all matches with string.
${var/pattern}Remove first match.
${var//pattern}Remove all matches.

Length

ParameterResult
${#VAR}Length of var in characters.
${#ARR[@]}Length of array in elements.

Expansion

ParameterResultCaveat
${VAR:OFFSET}Remove first N chars from variable.
${VAR:OFFSET:LENGTH}Get substring from N character to N character.
(${VAR:10:10}: Get sub-string from char 10 to char 20)
${VAR:: OFFSET}Get first N chars from variable.
${VAR:: -OFFSET}Remove last N chars from variable.
${VAR: -OFFSET}Get last N chars from variable.
${VAR:OFFSET:-OFFSET}Cut first N chars and last N chars.bash 4.2+

Case Modification

ParameterResultCavear
${VAR^}Uppercase first character.bash 4+
${VAR^^}Uppercase all characters.bash 4+
${VAR,}Lowercase first character.bash 4+
${VAR,,}Lowercase all characters.bash 4+
${VAR~}Reverse case of first character.bash 4+
${VAR~~}Reverse case of all characters.bash 4+

Default Value

ParameterResult
${VAR:-STRING}If VAR is empty or unset, use STRING as its value.
${VAR-STRING}If VAR is unset, use STRING as its value.
${VAR:=STRING}If VAR is empty or unset, set the value of VAR to STRING.
${VAR=STRING}If VAR is unset, set the value of VAR to STRING.
${VAR:+STRING}If VAR is not empty, use STRING as its value.
${VAR+STRING}If VAR is set, use STRING as its value.
${VAR:?STRING}Display an error if empty or unset.
${VAR?STRING}Display an error if unset.

Strings

Trim String

trim_string() {
    : "${1#"${1%%[![:space:]]*}"}"
    : "${_%"${_##*[![:space:]]}"}"
    printf '%s\n' "$_"
}

Trim + Monospace String

# shellcheck disable=SC2086,SC2048
trim_all() {
    set -f
    set -- $*
    printf '%s\n' "$*"
    set +f
}

Trim Quotes

trim_quotes() {
    # Usage: trim_quotes "string"
    : "${1//\'}"
    printf '%s\n' "${_//\"}"
}

Subshell Spawning

Declaring a function with regular brackets instead of curly braces ensures any instance of the content is treated as a subshell launch; the decision is given by the function itself, rather than having to disown the job / spawn a subshell at each call site.

some_func() {
    # do a serial thing
}

some_func() (
    # do a concurrent thing
)

Massively useful for generating concurrent operations, rather than dealing with the unpredictable & or disown.

Snippets

Rejecting Blank Input

[[ -z "$1" ]] && usage && exit 1

Where usage is a function that prints a hint or performs a default action.

Switching against Input Parameters

case "$1" in
    -s | --sequence) video_mode=false ;;
    -v | --video)    video_mode=true  ;;
    ?)               usage; exit 1    ;;
esac

This could also be achieved with wildcard expansion, but I like explicit specification — easier to work out what the hell you were thinking when you come back later:

case "$1" in
    -*s*) video_mode=false ;;
    -*v*) video_mode=true  ;;
    ?)    usage; exit 1    ;;
esac

Capture multiple values into an array

Use the double round brackets syntax, with the inner pair acting as the subshell.

image_size=($(identify -ping -format '%w %h' $1))

Pulling a string from a file as a variable

I use this one for making my source code's internally declared version numbers a source of truth for packaging releases. This is more of a grep thing than a bash thing, but I'll put it here until a better home reveals itself.

Given a Go or Odin codebase with the following declaration —

const VERSION = "v0.0.1"
VERSION :: "v0.0.1"

You can extract these with —

version=$(grep 'const VERSION' source/*.go | awk -F"[ \"]+" '/VERSION/{print $4}')
version=$(grep -r '^VERSION ::' source/*.odin | awk -F"[ \"]+" '{print $3}')

Note the added -r and ^ to use regex to match the start of the line; this is because I generally have multiple version declarations (save formats, package files, etc.) and the absence of any other syntax means it's easy to grab PACKAGE_VERSION and SAVE_FORMAT_VERSION and get multiple results. I could just name the base version better, but it's a long-standing convention of mine that I feel weirdly reluctant to break.

I also usually just point these at the file I know has the version to save a bunch of redundant work instead of globbing the entire source tree.

WORD COUNT
1322
LAST UPDATED
2025-12-20
BACKLINKS

Programming

Build Systems

Uses