Bourne Again Shell
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 "$@"
}
fiThis 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
| Parameter | Result |
|---|---|
${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
| Parameter | Result |
|---|---|
${#VAR} | Length of var in characters. |
${#ARR[@]} | Length of array in elements. |
Expansion
| Parameter | Result | Caveat |
|---|---|---|
${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
| Parameter | Result | Cavear |
|---|---|---|
${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
| Parameter | Result |
|---|---|
${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 1Where 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 ;;
esacThis 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 ;;
esacCapture 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.