Marcin
Marcin

Reputation: 49846

Ignore empty elements of bash indexed array

I'm filtering PATH variables, such that certain elements are deleted (in the example below, it's elements containing the string x86; in practice I'm looking for the string site-packages):

$ echo $(IFS=:;arr=($PATH);echo "${arr[*]//*x86*}")
/usr/local/bin:/usr/bin:/cygdrive/c/Program Files/Common Files/Microsoft Shared/Windows Live::/cygdrive/c/Program Files/Lenovo Fingerprint Reader:::/cygdrive/c/Program Files/Intel/iCLS Client:/cygdrive/c/Windows/system32:/cygdrive/c/Windows:/cygdrive/c/Windows/System32/Wbem:/cygdrive/c/Windows/System32/WindowsPowerShell/v1.0:/cygdrive/c/Program Files/Intel/Intel(R) Management Engine Components/DAL:/cygdrive/c/Program Files/Intel/Intel(R) Management Engine Components/IPT:::::/cygdrive/c/Program Files/Intel/WiFi/bin:/cygdrive/c/Program Files/Common Files/Intel/WirelessCommon:/cygdrive/c/Program Files/Common Files/Lenovo:::/cygdrive/c/SWTOOLS/ReadyApps:::::/usr/lib/lapack

This works pretty well, except that all of the elements which have been zeroed out are still, there, but empty. I'd like to end up with those elements eliminated, and I don't want to use anything other than bash to do it (I know I can use tr, but for portability reasons I would strongly prefer a bash-only solution).

Here's the tr-based solution:

echo $(IFS=:;arr=($PATH);echo "${arr[*]//*x86*}" | tr -s :)

Update: I'm using arrays because I want to be robust in the face of spaces (or anything else other than colons) in the path elements.

Upvotes: 2

Views: 2210

Answers (5)

fozzybear
fozzybear

Reputation: 157

A variation from the examples above, which doesn't require an additional subshell, preserves spaces in PATH and eliminates path separators along with its matches:

IFS="$IFS"; IFS=:
arr=($PATH)
"${arr[*]//*local*?(:|\/)/$'\b'}" &>/dev/null
printf "${_#*:}"
# OR: var="${_#*:}"
IFS="$_IFS"

It splits PATH at colons into array arr and expands it to string; then all paths between *...*, that end either with a colon : (Windows path separator), or slash / (last path entry), will be replaced with a backspace, deleting the path separator for the match. In case the first path entry matches, the remnant leading colon is removed from the result by ${_#*:}.

If used in a method, storing the result in tmp isn't required and echo "$_" should suffice.

Upvotes: 0

iruvar
iruvar

Reputation: 23364

This should work with assuming PATH constituents do not contain spaces, so definitely more fragile than the other solutions. The empty array elements are discarded at the point of arr2 assignment

echo $(IFS=:;arr=($PATH);unset IFS;arr2=(${arr[@]//*x86*});IFS=:;echo "${arr2[*]}")

Upvotes: 0

chepner
chepner

Reputation: 531345

This doesn't require an explicit loop, but might be a bit fragile. It uses a backspace to "unwrite" the colon preceding a directory to remove, and it requires special handling if the first or last directory in PATH is removed.

echo $(IFS=:;
       arr=($PATH);
       # When finally echoed, any \b character will remove
       # the preceding colon from the output.
       new="${arr[*]//*x86*/$'\b'}";
       # Remove :\b if last directory was removed
       new=${new%:$'\b'};
       # Remove \b: if first directory was removed
       echo "${new#$'\b':}")

Upvotes: 2

Marcin
Marcin

Reputation: 49846

Here's a version which builds up a contiguous array using a loop:

$ echo $(IFS=:;arr=($PATH);sqarr="${arr[*]//*x86*}";declare -a finalarr; for item in $sqarr; do [[ $item ]] && finalarr+=("$item"); done; echo "${finalarr[*]}")
/usr/local/bin:/usr/bin:/cygdrive/c/Program Files/Common Files/Microsoft Shared/Windows Live:/cygdrive/c/Program Files/Lenovo Fingerprint Reader:/cygdrive/c/Program Files/Intel/iCLS Client:/cygdrive/c/Windows/system32:/cygdrive/c/Windows:/cygdrive/c/Windows/System32/Wbem:/cygdrive/c/Windows/System32/WindowsPowerShell/v1.0:/cygdrive/c/Program Files/Intel/Intel(R) Management Engine Components/DAL:/cygdrive/c/Program Files/Intel/Intel(R) Management Engine Components/IPT:/cygdrive/c/Program Files/Intel/WiFi/bin:/cygdrive/c/Program Files/Common Files/Intel/WirelessCommon:/cygdrive/c/Program Files/Common Files/Lenovo:/cygdrive/c/SWTOOLS/ReadyApps:/usr/lib/lapack

Across multiple lines:

$(
  IFS=:
  arr=($PATH)
  sqarr="${arr[*]//*x86*}"
  declare -a finalarr
  for item in $sqarr; do 
      [[ $item ]] && finalarr+=("$item")
  done 
  echo "${finalarr[*]}"
)

Upvotes: 0

glenn jackman
glenn jackman

Reputation: 246867

You'll have to be more explicit about it:

PATH=$(
  IFS=:
  new=()
  for p in $PATH; do
    [[ $p != *x86* ]] && new+=("$p")
  done
  echo "${new[*]}"
)

It's more verbose, but correct and bash-only as requested

Upvotes: 3

Related Questions