prepending to the $PATH - bash

In order to avoid ad-hoc setting of my PATH by the usual technique of blindly appending - I started hacking some code to prepend items to my path (asdf path for example).
pathprepend() {
for ARG in "$#"
do
export PATH=${${PATH}/:$"ARG"://}
export PATH=${${PATH}/:$"ARG"//}
export PATH=${${PATH}/$"ARG"://}
export PATH=$ARG:${PATH}
done
}
It's invoked like this : pathprepend /usr/local/bin and /usr/local/bin gets prepended to PATH. The script is also supposed to cleanly remove /usr/local/bin from it's original position in PATH (which it does, but not cleanly)(dodgy regex).
Can anyone recomend a cleaner way to do this? The shell (bash) regex support is a bit limited. I'd much rather split into an array and delete the redundant element, but wonder how portable either that or my implementation is. My feeling is, not particularly.

If you want to split PATH into an array, that can be done like so:
IFS=: eval 'arr=($PATH)'
This creates an array, arr, whose elements are the colon-delimited elements of the PATH string.
However, in my opinion, that doesn't necessarily make it easier to do what you want to do. Here's how I would prepend to PATH:
for ARG in "$#"
do
while [[ $PATH =~ :$ARG: ]]
do
PATH=${PATH//:$ARG:/:}
done
PATH=${PATH#$ARG:}
PATH=${PATH%:$ARG}
export PATH=${ARG}:${PATH}
done
This uses bash substitution to remove ARG from the middle of PATH, remove ARG from the beginning of PATH, remove ARG from the end of PATH, and finally prepend ARG to PATH. This approach has the benefit of removing all instances of ARG from PATH in cases where it appears multiple times, ensuring the only instance will be at the beginning after the function has executed.

Related

bash array slicing strange syntax in perl path: `${PATH:+:${PATH}}"`

On Linux Ubuntu, when you do sudo apt update && sudo apt install perl, it adds the following to the bottom of your ~/.bashrc file (at least, many months later, I think that is what added those lines):
PATH="/home/gabriel/perl5/bin${PATH:+:${PATH}}"; export PATH;
PERL5LIB="/home/gabriel/perl5/lib/perl5${PERL5LIB:+:${PERL5LIB}}"; export PERL5LIB;
PERL_LOCAL_LIB_ROOT="/home/gabriel/perl5${PERL_LOCAL_LIB_ROOT:+:${PERL_LOCAL_LIB_ROOT}}"; export PERL_LOCAL_LIB_ROOT;
PERL_MB_OPT="--install_base \"/home/gabriel/perl5\""; export PERL_MB_OPT;
PERL_MM_OPT="INSTALL_BASE=/home/gabriel/perl5"; export PERL_MM_OPT;
What does this strange syntax do in many of the lines, including in the first line? It appears to be some sort of bash array slicing:
${PATH:+:${PATH}}
The ${PATH} part is pretty straightforward: it reads the contents of the PATH variable, but the rest is pretty cryptic to me.
It's not array slicing; it's a use of one of the POSIX parameter expansion operators. From the bash man page, in the Parameter Expansions section,
${parameter:+word}
Use Alternate Value. If parameter is null or unset, nothing is
substituted, otherwise the expansion of word is substituted.
It's a complex way of making sure that you only add a : to the value if PATH isn't empty to start with. A longer, clearer way of writing it would be
if [ -n "$PATH" ]; then
PATH=/home/gabriel/perl5/bin:$PATH
else
PATH=/home/gabriel/perl5/bin
fi
However, since it if almost inconceivable that PATH is empty when .basrhc is sourced, it would be simpler to just prepend the new path and be done with it.
PATH=/home/gabriel/perl5/bin:$PATH
If PATH actually ended with a :, it would implicitly include the current working directory in the search path, which isn't a good idea for security reasons. Also from the bash man page, in the section on Shell Variables under the entry for PATH:
A zero-length (null) directory name in the
value of PATH indicates the current directory. A null directory
name may appear as two adjacent colons, or as an initial or
trailing colon.
As an aside, it's good to understand what various installers try to add to your shell configuration. It's not always necessary, and sometimes can actively change something you already have configure.
I would much prefer if packages simply printed instructions for what needs to be added to your configuration (and why), and leave it to the user to make the appropriate modifications.
What does this strange syntax do in many of the lines, including in the first line?
It's the ${parameter:+word} form of parameter expansion where word becomes the expanded value if parameter is not unset and not having the value of an empty string (a.k.a. null).

How to obtain the full PATH, *allowing* for symbolic links

I have written bash scripts that accept a directory name as an argument. A single dot ('.') is a valid directory name, but I sometimes need to know where '.' is. The readlink and realpath commands provide a resolved path, which does not help because I need to allow for symbolic links.
For example, the resolved path to the given directory might be something like /mnt/vol_01/and/then/some, whereas the script is called with '.' where '.' is /app/then/some (a sym link which would resolve to the first path I gave).
What I have done to solve my problem is use cd and pwd in combination to provide the full path I want, and it seems to have worked OK so far.
A simplified example of a script:
DEST_DIR=$1
# Convert the given destination directory to a full path, ALLOWING
# for symbolic links. This is necessary in cases where '.' is
# given as the destination directory.
DEST_DIR=$(cd $DEST_DIR && pwd -L)
# Do stuff in $DEST_DIR
My question is: is my use of cd and pwd the best way to get what I want? Or is there a better way?
If all you want to do is to make an absolute path that has minimal changes from a relative path then a simple, safe, and fast way to to it is:
[[ $dest_dir == /* ]] || dest_dir=$PWD/$dest_dir
(See Correct Bash and shell script variable capitalization for an explanation of why dest_dir is preferable to DEST_DIR.)
The code above will work even if the directory doesn't exist (yet) or if it's not possible to cd to it (e.g. because its permissions don't allow it). It may produce paths with redundant '.' components, '..' components, and redundant slashes (`/a//b', '//a/b/', ...).
If you want a minimally cleaned path (leaving symlinks unresolved), then a modified version of your original code may be a reasonable option:
dest_dir=$(cd -- "$dest_dir"/ && pwd)
The -- is necessary to handle directory names that begin with '-'.
The quotes in "$dest_dir" are necessary to handle names that contain whitespace (actually $IFS characters) or glob characters.
The trailing slash on "$dest_dir"/ is necessary to handle a directory whose relative name is simply -.
Plain pwd is sufficient because it behaves as if -L was specified by default.
Note that the code will set dest_dir to the empty string if the cd fails. You probably want to check for that before doing anything else with the variable.
Note also that $(cd ...) will create a subshell with Bash. That's good in one way because there's no need to cd back to the starting directory afterwards (which may not be possible), but it could cause a performance problem if you do it a lot (e.g. in a loop).
Finally, note that the code won't work if the directory name contains one or more trailing newlines (e.g. as created by mkdir $'dir\n'). It's possible to fix the problem (in case you really care about it), but it's messy. See How to avoid bash command substitution to remove the newline character? and shell: keep trailing newlines ('\n') in command substitution. One possible way to do it is:
dest_dir=$(cd -- "$dest_dir"/ && printf '%s.' "$PWD") # Add a trailing '.'
dest_dir=${dest_dir%.} # Remove the trailing '.'

GOBIN root setting with var multi GOPATH in .zshrc config

export GOPATH=~/mygo:~/go
export GOBIN=$GOPATH/bin
I expected the $GOBIN equals ~/mygo/bin:~/go/bin but it is ~/mygo:~/go/bin instead.
how could I set them a better way? thx
Solution
export GOPATH=~/mygo:~/go
export GOBIN=${(j<:>)${${(s<:>)GOPATH}/%//bin}}
Explanation
Although whatever program uses GOPATH might interprete it as an array, for zsh it is just a scalar ("string").
In order to append a string (/bin) to every element the string "$GOPATH" first needs to be split into an array. In zsh this can be done with the parameter expansion flag s:string:. This splits a scalar on string and returns an array. Instead of : any other character or matching pairs of (), [], {} or <> can be used. In this case it has to be done because string is to be :.
GOPATH_ARRAY=(${(s<:>)GOPATH)
Now the ${name/pattern/repl} parameter expansion can be used to append /bin to each element, or rather to replace the end of each element with /bin. In order to match the end of a string, the pattern needs to begin with a %. As any string should be matched, the pattern is otherwise empty:
GOBIN_ARRAY=(${GOPATH_ARRAY/%//bin})
Finally, the array needs to be converted back into a colon-separated string. This can be done with the j:string: parameter expansion flag. It is the counterpart to s:string::
GOBIN=${(j<:>)GOBIN_ARRAY}
Fortunately, zsh allows Nested Substitution, so this can be done all in one statement, without intermediary variables:
GOBIN=${(j<:>)${${(s<:>)GOPATH}/%//bin}}
Alternative Solution
It is also possible to do this without parameter expansion flags or nested substitution by simply appending /bin to the end of the string and additionally replace every : with /bin::
export GOBIN=${GOPATH//://bin:}/bin
The ${name//pattern/repl} expansion replaces every occurence of pattern with repl instead of just the first like with ${name/pattern/repl}.
This would also work in bash.
Personally, I feel that it is a bit "hackish", mainly because you need to write /bin twice and also because it completely sidesteps the underlying semantics. But that is only personal preference and the results will be the same.
Note:
When defining GOPATH like you did in the question
export GOPATH=~/mygo:~/go
zsh will expand each occurence of ~/ with your home directory. So the value of GOPATH will be /home/kevin/mygo:/home/kevin/go - assuming the user name is "kevin". Accordingly, GOBIN will also have the expanded paths, /home/kevin/mygo/bin:/home/kevin/go/bin, instead of ~/mygo/bin:~/go/bin
This could be prevented by quoting the value - GOPATH="~/mygo:~/go" - but I would recommend against it. ~ as synonym for the home directory is not a feature of the operating system and while shells usually support it, other programs (those needing GOPATH or GOBIN) might not do so.

zsh Looping through multiple parameters

In my old .bashrc, I had a short section as follows:
PATH2ADD_SCRIPTBIN="/home/foo/bar/scriptbin"
PATH2ADD_PYTHONSTUFF="/home/foo/bar/pythonprojects"
PATH2ADDLIST="$PATH2ADD_SCRIPTBIN $PATH2ADD_PYTHONSTUFF"
for PATH2ADD in $PATH2ADDLIST; do
if [ -z `echo $PATH | grep "$PATH2ADD"` ]; then
export PATH=$PATH:$PATH2ADD
echo "Added '$PATH2ADD' to the PATH."
fi
done
And in Bash, this worked just as intended: it appended the paths I included in $PATH2ADDLIST if they were not already present in the path (I had to do this after realizing how huge my path was getting each time I was sourcing my .bashrc). The output (when the provided paths were not already present) was as follows:
Added '/home/foo/bar/scriptbin' to the PATH.
Added '/home/foo/bar/pythonprojects' to the PATH.
However, I recently switched over to the magical land of Zsh, and the exact same lines of text now produce this result:
Added '/home/foo/bar/scriptbin /home/foo/bar/pythonprojects' to the PATH.
Now I'm pretty sure that this is because of some difference in how Zsh does parameter expansion, or that it has something to do with how Zsh changes the for loop, but I'm not really sure how to fix this.
Might anyone have some insight?
Use an array to store those variables, i.e.
PATH2ADD_SCRIPTBIN="/home/foo/bar/scriptbin"
PATH2ADD_PYTHONSTUFF="/home/foo/bar/pythonprojects"
# Initializing 'PATH2ADDLIST' as an array with the 2 variables
# to make the looping easier
PATH2ADDLIST=("${PATH2ADD_SCRIPTBIN}" "${PATH2ADD_PYTHONSTUFF}")
# Looping through the array contents
for PATH2ADD in "${PATH2ADDLIST[#]}"
do
# Using the exit code of 'grep' directly with a '!' negate
# condition
if ! echo "$PATH" | grep -q "$PATH2ADD"
then
export PATH=$PATH:$PATH2ADD
echo "Added '$PATH2ADD' to the PATH."
fi
done
This way it makes it more compatible in both zsh and bash. A sample dry run on both the shells,
# With interpreter set to /bin/zsh
zsh script.sh
Added '/home/foo/bar/scriptbin' to the PATH.
Added '/home/foo/bar/pythonprojects' to the PATH.
and in bash
bash script.sh
Added '/home/foo/bar/scriptbin' to the PATH.
Added '/home/foo/bar/pythonprojects' to the PATH.
zsh has a few features that make it much easier to update your path. One, there is an array parameter path that mirrors PATH: a change to either is reflected in the other. Two, that variable is declared to eliminate duplicates. You can simply write
path+=("/home/foo/bar/scriptbin" "/home/foo/bar/pythonprojects")
and each new path will be appended to path if it is not already present.
If you want more control over the order in which they are added (for example, if you want to prepend), you can use the following style:
path=( "/home/foo/bar/scriptbin"
$path
"/home/foo/bar/pythonprojects"
)
(Note that the expansion of an array parameter includes all the elements, not just the first as in bash.)

how to filter a command subsitution from the resulting value of a readlink for symlink?

This may be poorly titled as I'm not fully sure what the process is called.
Basically I want to get only the last part of a symlink path, and I'm trying to use the same method I use with PWD.
For example:
if I do
PWD
it prints
/opt/ct/mydir
if I do
echo ${PWD##*/}
it prints only the last part
mydir
So using that design I can do
readlink mysymlink
which gives
/opt/ct/somedir
and I can do
TMP=$(readlink mysymlink)
echo ${TMP##*/}
and it will print
somedir
So now how can I combine that last part into one line like
TMP=$(readlink mysymlink && echo ${TMP##*/})
???
The example I show gives me 2 concatenated results.. one with the full path and one with just the part I want. I only want that last directory.
I also tried
TMP=${ $(readlink mysymlink)##*/}
to no avail
Variable substitution suffixes can only be used with variables, not command substitutions. You either have to set the variable and modify it in separate statements, as in your first attempt, or use additional command substitutions:
TMP=$(basename $(readlink))

Resources