zsh Looping through multiple parameters - bash

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.)

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).

prepending to the $PATH

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.

Bash check if path contains two folders

How can I search an arbitrary path and determine if it has two folder names? The folder names can appear in any position in either order. Not a shell expert so seeking help here.
if [ -p "$PATH" ]; then
echo "path is set"
else
echo "path is not set"
fi
I found this segment but I'm not sure it's useful. $PATH is a special variable correct?
First, let me make sure I understand the question right. You have some path (like "/home/sam/foo/bar/baz") and you want to test whether it contains two specific directory names (e.g. "foo" and "bar") in either order, right? So, looking for "foo" and "bar":
/home/sam/foo/bar/baz would match
/mnt/bar/subdir/foo would also match
/mnt/bar/foo2 would not match, because "foo2" is not "foo"
If that's correct, you can do this in bash as two tests:
dir1="foo"
dir2="bar"
if [[ "/$path/" = *"/$dir1/"* && "/$path/" = *"/$dir2/"* ]]; then
echo "$path" contains both $dir1 and $dir2"
else
echo "$path" does not contain both $dir1 and $dir2"
fi
Notes:
This is using the [[ ]] conditional expression, which is different from [ ] and not available in basic shells. If you use this type of expression, you need to start the shell script with a shebang that tells the OS to run it with bash, not a generic shell (i.e. the first line should be either #!/bin/bash or #!/usr/bin/env bash), and do not run it with the sh command (that will override the shebang).
The way the comparison works is that it sees whether the path matches both the patterns *"/$dir1/"* and *"/$dir2/"* -- that is, it matches those names, with a slash at each end, maybe with something else (*) before and after. But since the path might not start and/or end with a slash, we add them ("/$path/") to make sure they're there.
Do not use PATH as a variable in your script -- it's a very special variable that tells the shell where to find executable commands. If you ever use it for anything else, your script will suddenly start getting "command not found" errors. Actually, there are a bunch of all-caps special-meaning variables; to avoid conflicts with them, use lowercase or mixed-case variables for your things.

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))

Substitute output from variable to give another output

I am trying to substitute output from variable to give another output. The variable i have problems with is the $apps. It gives me "syntax error: bad substitution".
$appletDir is a directory with desktop shortcuts. The problem is that some shortcuts do not have the same name as the icon(png). So i need to substitute the program name with the png linking to it. I got it working with the commented out if-statement below. If this substitution could work then my script would look better. Cause i need to put down a couple of this.
I want it to look for "general_call" instead of "rtcom-call-ui" when going through the icon folders. Cause the png is called "general_call". The icons folders are the variables $icoDir64 $icoDirSca.
for applet in $appletDir*
do
app=`basename $applet | sed -e 's/.*://g' -e 's/.*osso-//g' -e 's/\.desktop.*//g'`
apps="${app/rtcom-call-ui/general_call}"
#if [ "${app}" = "rtcom-call-ui" ]; then
# app="general_call"
#fi
#echo $apps
#done
#exit 0
found=`find ${icoDir64} ${icoDirSca} -name "*.png"`
for file in $found
do
base="`basename ${file}`"
if [ "${base}" = "${app}.png" -o "${base}" = "tasklaunch_${app}.png" -o "${base}" = "general_${app}.png" ]; then
echo "WORKING!!!!!!!!!!!!!!!!!! $file"
fi
done
done
I think you may have a shell version problem (your shell isn't as modern as the notation you are using). A previous incarnation of this post suggested:
apps="${app}/rtcom-call-ui/general_call"
Or, for substituting rtcom-call-ui with general_call, you need to use echo and sed (at least in classic shells - it might be that bash has something built-in to do it):
apps=$(echo "${app}" | sed s/rtcom-call-ui/general_call/)
The notation ${var|continuation} (where | represents an arbitrary punctuation character) is used to modify the value substituted. For example:
apps="${app:-/something/suitable/as/the/default}"
would copy the value of $app, unless $app is not set at all (not relevant here; useful with environment variables) or if $app is an empty string.
The error you are getting is because there is no valid substitution that starts with '/' in your version of the shell. This notation seems to be valid in some versions of Bash (including the one I have to play with); I don't know when it was added. But if the shell you are using is complaining about the notation, then clearly it is not correct for the version of the shell you are using.
Depending on the shebang line (#!/bin/sh vs #!/bin/bash), it might work differently. Failing that, the version of Bash on your machine may be too old.
You can check your shell(s) with:
for app in /some/location/rtcom-call-ui/where.png /another/location/nowhere/thing.png
do
apps=${app/rtcom-call-ui/general-call}
echo $app
echo $apps
done

Resources