Shell – less convoluted way to set the $path array locally within function

scriptingshell-scriptzsh

Is there a less laborious way for setting a local version of the $path array than what's shown in the following snippet?

foo () {
    local holdpath
    holdpath=($path)
    local path
    path=($holdpath)
    if ( some_condition ) path=( $PREFIX $path )

    # do stuff
}

I'm referring specifically to the song-and-dance with holdpath

If instead I define

foo () {
    local path=($path)
    if ( some_condition ) path=( $PREFIX $path )

    # do stuff
}

…the first assignment to path triggers a bad pattern error. Of course, if instead I define

foo () {
    local path
    path=($path)
    if ( some_condition ) path=( $PREFIX $path )

    # do stuff
}

…the first assignment to path makes no difference (i.e. it can be omitted without altering the results); with or without it, $path will be empty.

EDIT:

The following script tests the various suggestions I've gotten so far:

foo_0 () {
  echo ${#path}
  local PATH=$PATH
  echo ${#path}
}

foo_1 () {
  echo ${#path}
  eval "local path; path=(${(q)path})"
  echo ${#path}
}

foo_2 () {
  echo ${#path}
  eval "$(local -p path)"
  echo ${#path}
}

for i ( 0 1 2 ) {
  fn=foo_$i
  echo "# $fn"
  $fn
  echo
}

The output is:

# foo_0
22
22

# foo_1
22
1

# foo_2
22
22

So the outputs for foo_0 and foo_2 are at least consistent with what I'm trying to achieve. Something is not working with foo_1 as given above, but I get rid of the (q) in the assignment to path, i.e.

foo_1 () {
  echo ${#path}
  eval "local path; path=($path)"
  echo ${#path}
}

…then the output agrees with that of foo_0 and foo_2. Unfortunately, even after reading the documentation for the q qualifier a couple of times, I don't quite understand what it's supposed to be doing in the original recipe.

Also, I cannot understand why the following command-line variant of foo_0 differs from the one above:

% (foo_0a () { echo ${#path}; local PATH=$PATH; echo ${#path} }; foo_0a)
22
1

FWIW, the corresponding command-line variants of foo_1 and foo_2 produce the same results as their originals in the script:

% (foo_1a () { echo ${#path}; eval "local path; path=(${(q)path})"; echo ${#path} }; foo_1a)
22
1
% (foo_2a () { echo ${#path}; eval "$(local -p path)"; echo ${#path}; }; foo_2a)
22
22

Also, in all the cases above in which echo ${#path} produces 1 instead of 22 the reason is that the local $path variable contains all the individual paths in a single string, separated by spaces.

Best Answer

While for tied arrays, you can use rici's answer, in the general case, you could do:

foo() {
  eval "local array; array=(${(q)array[@]})"
  ...
}

The (q) is to quote the elements of the arrays. For instance, for a value of $PATH like /foo bar:/x$y, "${(q)path[@]}" would expand to /foo\ bar /x\$y. We need to escape those space and dollar characters because that string is passed to eval.

You could also do:

foo() {
  eval "$(local -p array)"
  ...
}

which would work with any type of variable, but that forks an extra process.

Related Question