TIL: Don't set a variable named path inside functions when using zsh

lucassperez

Lucas Perez

Posted on March 20, 2024

TIL: Don't set a variable named path inside functions when using zsh

🇧🇷🇵🇹 Versão em português aqui.

In the past, I used to use aliases for common things, sometimes a sequence of commands or commands with specific flags and options. But when it got too complicated, I used to create shell scripts to handle more complex logic. Then I would create aliases that just call these shell scripts. I prefered this over putting the scripts in my path because then I could name then with more descriptive names and I wouldn't have to deal with the PATH variable.

But at some point I realized that I can just create functions and put then in my shell config file (like .bashrc, .zshrc etc) and then I would have these functions available to me. Also, using the script+alias aproach, running which <command> wouldn't be all that useful, it would just show me something like command=/path/to/script. But when I define functions, running which <function> shows me the implementation of it.

I think the behaviour of which is not the same on every distro, but on Fedora it does that.

Of course that if the implementation is several lines long, it might not be helpful anyways, but anyways.

You can pretty much copy paste your shell scripts into functions definitions, just have in mind that instead of using exit, you should use return.

Or can you?

Today I wanted to create a function that would call curl to get an authorization token, and then use this token to make another curl to a supplied route, so I wouldn't have to copy paste around a token the whole day and deal with yet another --header flag in curl just to develop my API.

So I did something like this:

my-api-curl() {
  token=`curl ... | jq -r '.token'`
  path="$1"
  shift
  curl \
    --header "Authorization: Bearer $token" \
    --url "http://localhost:3000/$path" \
    "$@"
}
Enter fullscreen mode Exit fullscreen mode

What I wanted was to supply the path as the first argument to the function and append it to localhost:3000 (so I don't have to type so much all the time. We're lazy, right?). And then I would like to pass curl flags to my function so it would repass it to curl internaly. So I grab the first argument with $1, I do a shift to remove it from the arg list, and pass the rest as is to curl.

So I can do things like this:

# Do a GET to http://localhost:3000/posts
my-api-curl /posts
Enter fullscreen mode Exit fullscreen mode

But I can also do things like this:

# Do a POST to http://localhost:3000/posts with that body
my-api-curl /posts -X POST -d '{"title": "Nice Post"}'
Enter fullscreen mode Exit fullscreen mode

This makes my function flexible, I just have to know that the FIRST argument is the url. I could iterate over the arguments, use getopts or something, but I was doing something dirt and quick.

So when I was going to run this function, I got a weird error:

$ my-api-curl /posts
curl: Command not found
Enter fullscreen mode Exit fullscreen mode

I thought, what? I do have curl. Lets edit the file where I define it:

vim ~/.zshrc
vim: Command not found
Enter fullscreen mode Exit fullscreen mode

Pretty much every command I tried from here on would say Command not found, I could not use ls, cd etc.

What is going on? Why didn't my function work, and why did it bork my terminal session?

The error message is not very clear. I wanted to run it as a script with the -x flag for debugging purposes, but when I copy pasted it into a file and ran it with sh, it just worked! No problems with it!

After some scratching my head, I had a hunch that the line path=... was troublesome. I renamed the variable to simply p and now the function works.

And even weirder, I tried the same using our trusty old friend bash, and I had no problem with using a variable named path. So I guess there is some weird zsh magic going on there.

And by the way, I updated my function to use local p=... when defining the variable. When doing so, it does not bork my terminal session if I use local path=..., but the function itself does not work. Interesting. And then I thought, maybe zsh can use both path and PATH?

Well, it is true! In zsh, if you echo $path, you'll get the same thing as echo $PATH! So there you go, since I was using a variable named path, it was overriding my PATH env var! That's why using local saved my terminal, because when not using it, this overwritten value would affect everything, not just the function.

In bash, echo $path just outputs nothing. I also tried in zsh to echo $home, but this outputs nothing as well. So weirdly, zsh is case insensitive only for the path variable. It is not case insensitive for any other variable I tried, be it x, myvariable or shell.

All my scripts shebangs are /sh though, so that's why I didn't have a problem when I copy pasted my function to a script, it worked well, because whatever /sh is in my computer, it is not case insensitive for my PATH variable.

And while writing this, I decided to try some googling (I didn't while I was solving the problem, because I like figuring things on my own. We're just lazy to type, not to debug, right?), it looks like this is also the behaviour of csh and tcsh. Also, zsh is kind of based on tcsh and ksh I think (don't quote me on that), so maybe that's where it comes from.

Also, zsh is not actually case insensitive, since pAth, PAtH etc does not work. It simply understands both path and PATH. I learned that with further experiments.

To be honest, I thing this is weird and kind of hurts the principle of least surprises, but I guess today I learned.

TIL zsh is somewhat case insensitive for the PATH.
Also, use local in your functions!

💖 💪 🙅 🚩
lucassperez
Lucas Perez

Posted on March 20, 2024

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related