ZSH filepath modifiers

zsh comes bundled with variable modifier to alter filepaths and extract the relevant parts.

Given the following code, we can display $filepath in a lot of different ways:

mkdir -p /tmp/subdir
cd /tmp
local filepath=./subdir/file.zsh

| Name | Output | Modifier | Mnemonic | | --------- | ---------------------- | ----------------- | ------------------ | | Absolute | /tmp/subdir/file.zsh | ${filepath:a} | absolute | | Basename | file.zsh | ${filepath:t} | tail | | Filename | file | ${filepath:t:r} | tail rest | | Extension | zsh | ${filepath:e} | extension | | Dirpath | /tmp/subdir | ${filepath:a:h} | absolute h ead |

For clarity

  • tail is everything after the last /
  • head is everything before the last /
  • extension is everything after the last .
  • rest is everything before the last .

Other goodies

  • ${~filepath} will expand ~ to their full path, while ${filepath/#$HOME/\~} will use ~ instead of home path
  • :command gives you the executable path of a command (a bit like which)
  • :q for quoting, :U for unquoting, :x for quoting individual words
  • :l for lowercase, :u for uppercase
  • :2:10 takes a substring from 2 to 10
  • Those modifiers can be applied directly to glob patterns (src/**/*.zsh(:t:r))

Default variable values with zsh

zsh has two modifiers (${:-} and ${:=}) to handle fallback for empty values.

echo ${ahead:-0} will display the variable $ahead, or display 0 if the variable is empty.

echo ${ahead:=0} (note the := instead of :-) will assign 0 to the variable $ahead, and then display it.

They are pretty similar, and in that example the result is the same. But with :=, the variable will still be set to 0 afterward, while with :- it's a one-off thing.

Search and replace with zsh

To search and replace with zsh, there are two ways.

With ${var//XXX/YYY}

This will replace all occurences of XXX in $var with YYY.

Note that you can interpolate variables inside of XXX, so ${var//${input}/YYY} with input="foo" will replace foo with YYY in $var.

You can use only one / instead of // to only replace the first occurence.

With ${var:gs/XXX/YYY}

The :s/XXX/YYY modifier is the basic syntax to replace XXX with YYY.

It does not allow for interpolating variables. You need to replace s with gs to make it a global search and replace.

The only advantage over the other syntax in my opinion is that you can swap the delimiter character (/) with any other character you like. So if your patterns are heavy on /, you can swap them for _ for example for a more readable format, like ${var:gs_/_#} to replace all / with #.

Thanks, HTTrack

Sometimes, you need to download a whole website locally. And for that, HTTrack is the best tool I ever found.

Sure, you could use wget and some recursion to get what you want, but HTTrack solves all that for you.

Reasons you might need to download a whole website: - You need to browse it when you don't have an internet connection (on the go, with low data usage, in a train, etc) - You want to run scripts on the content, and it's faster with a local copy than a remote one - You want to make a copy of a website that might disappear

Scoping zsh variables

By default in zsh, if you define a local myVariable it will be available to the whole script running it.

Any of those variables defined in your .zshrc will also be visible in your terminal. This can create weird bugs when you accidentally defined a variable with the same name as another zsh script.

Note that those are not environment variables. Even if you can read them from your zsh terminal, they are not accessible from other tools. To explicitly make them available as environment variables, you need to export them.

If you define a variable inside a function, it stays scoped to that function, though. Also, anonymous functions are run as soon as they are defined, and discarded afterwards.

Using those two features, we can define our variables without them being available in the terminal global scope.

I find it a best practice to wrap any of my sourced zsh scripts in a function () { } block, like this:

local one=1
function () {
  local two=2
  export THREE=3
}

one is accessible in the whole .zsh script, including inside of the function, and even during your whole terminal session (you can echo $one). Other, non-zsh, scripts won't be able to read it though.

two is accessible only in the body of the function, and is scoped there and won't be accessible from outside. It's perfect for small variables you need to simplify your code but don't need laying around.

THREE is an environment variable, that can be used in the terminal (echo $THREE) as well as in any other script you're running from the terminal. It's useful if you need to set some global flags or editing global settings (like the $PATH variable).