Hacker News new | ask | show | jobs
by nerdponx 1630 days ago
Most notably you will likely end up with duplicated elements in PATH unless you take specific steps to prevent double-adding.

For example, I have this in my (relatively complicated) shell config:

    # Don't double-add $PATH entries
    _not_yet_in_path() {
        case "$PATH" in
              $1:*) return 1 ;;
            *:$1  ) return 1 ;;
            *:$1:*) return 1 ;;
                 *) return 0 ;;
        esac
    }

    # Only absolute paths to currently-existing directories are be allowed in $PATH
    _can_add_to_path() {
        case "$1" in
            *:*) return 1 ;;
             /*) test -d "$1" && _not_yet_in_path "$1" ;;
              *) return 1 ;;
        esac
    }

    prepend_to_path() {
        if _can_add_to_path "$1"
        then
            export PATH="${1}${PATH+:$PATH}"
        fi
    }

    append_to_path() {
        if _can_add_to_path "$1"
        then
            export PATH="${PATH+$PATH:}${1}"
        fi
    }

The full script is here: <https://git.sr.ht/~wintershadows/dotfiles/tree/master/item/....>. Feedback is always welcome on how I can make this better! The Zsh version of this is a lot nicer.
5 comments

That's a lot of work for avoiding double PATHing. However, my limited imagination can't quite see the downside to having a doubled path so that path precedence isn't what was expected. Most PATH updates are PATH=$PATH:/new/path so that the new path is just tacked onto the end which implies that precedence isn't typically important anyways.
The problem with "double PATHing" is that some things get doubled and some things don't. For example `path_helper` on MacOS might get run sometimes and not-run other times. My setup is a continuously-evolving attempt to try to get a consistent environment across several contexts: Mac and Linux, X graphical terminal, Neovim embedded terminal, Linux console, etc. Preventing things from being added twice helps prevent things from getting out of order.
The out of order argument doesn't carry much water for me though. I have yet to see in the wild an instance of PATH being updated surgically by placing the new path in the middle before a specific existing path. It's always just tacked onto the end. Maybe I've seen it prepended to the front PATH=/new/path:$PATH.

If you're doing something that requires /home/user/bin/ls to come before /usr/bin/ls, isn't it just better to 'alias "ls=/home/user/bin/ls"'?

Kind of. This case is a bit funky, because I tend to use "non-system" package managers like Pkgsrc and Brew, as well as tools like ASDF-VM and Conda. However I don't always install or set up all these tools on all machines.

It all tends to operate in "layers", mostly governed by the sequence of PATH entries. Usually there are too many individual programs to alias, and often which version I want to use is determined dynamically.

Ultimately doing things the way I do them makes sense for me, but it's definitely not very simple and I wouldn't recommend it for most people.

Wouldn't it be simpler to just add everything into your path, then split, uniq, and re-combine it, at the end?

Edit:

I use a .bashrc.d directory to store all of my bash customizations. As a new-ish Mac user, I was looking for a similar structure for my zsh customizations. Really like your setup - thanks for sharing!

Happy it helped! Indeed, you can see in the Zsh config it's all a lot simpler, taking advantage of `typeset -T` among other things.
I personally just completely overwrite $PATH in my .bashrc. They aren't adding new system directories for executables, after all. It hasn't changed since like 1970 :)

    case ":$PATH:" in
        *:$1:*) return 1 ;;
             *) return 0 ;;
    esac

?
What if the entry is already present, but at the end or the beginning?
I use something like this:

  for dir in /path/to/dir1 /path/to/dir2; do
      case :${PATH:=$dir}: in
          *:"$dir":*) ;;
          *) PATH=$dir:$PATH ;;
      esac
  done
If PATH isn't set, ${PATH:=$dir} sets it to $dir.

  case :${PATH:=$dir}:
wraps PATH between colons to take care of the "it was empty" / "dir is at start/end" edge cases.

The first case is hit when the PATH contained the directory already (no-op); the second case prepends the new directory to the PATH.

Beware that PATH being unset or empty is a special case and does not necessarily mean that no directories are searched. Different shells handle it differently, and other utilities spawned by the shells handle it themselves in a way that may or may not match the shell. Whether setting PATH to $dir if it was previously unset or empty is the right thing to do is therefore a matter of opinion. Personally, I would stay out of that mess and either issue an error or leave PATH unmodified.
That's what the extra :s in ":$PATH:" are for: they prepend and append an empty component to the list. Any component in the original "$PATH" will still be in ":$PATH:" too, but no longer be at the end or the beginning, allowing the pattern to be simplified.
Well, the real solution here is to set them in ~/.profile.
My "dotfiles" are used across MacOS (where all shells are login shells by default), X11, and Wayland. So I have some extra layers of safety just in case I mess something up.