0

Edit: Mostly rewritten for clarity/digestibility. Also, I found one possible answer using xargs. I prefer avoiding the external command--I'll only accept my own answer if no alternative turns up.

I'm trying to pass the output of local (declared local variables as a newline-delimited key=value list), back to another invocation of local, but I'm bumping up against the edge of my understanding of bash quoting/escaping.

It's easy to handle values with no spaces, but values with internal spaces cause quoting/splitting trouble. Easier to show than tell. Using BASH_VERSION=4.4.23(1)-release. I'll walk through the steps; there's a longer repro script at the end of the post.

  1. Define some local variables, and print them with the local builtin:
    function export_locals(){
        local one=1
        local two=$(echo word1 word2)
        local three=$(echo "word1 ain't word2")
        local # aka local -p
    }
    
    Which outputs some variation of:
    one=1
    three='word1 ain'\''t word2'
    two='word1 word2'
    
  2. Call export_locals from another function, and pass the output to another invocation of the local builtin:
    function import_locals(){
        local $(export_locals)
        local
    }
    
    With the default IFS, this will fail. set -x shows the cause:
    + local one=1 'three='\''word1' 'ain'\''\'\'''\''t' 'word2'\''' 'two='\''word1' 'word2'\'''
    bash: local: `ain'\''t': not a valid identifier
    bash: local: `word2'': not a valid identifier
    bash: local: `word2'': not a valid identifier
    
    Using IFS=$'\n' keeps it from splitting the values of two or three, but they end up with bonus single quotes:
    + local one=1 'three='\''word1 ain'\''\'\'''\''t word2'\''' 'two='\''word1 word2'\'''
    + local
    one=1
    three=''\''word1 ain'\''\'\'''\''t word2'\'''
    two=''\''word1 word2'\'''
    
    At this point echo $two would output 'word1 word2'

I'm looking for ways to remove the spare quotes that won't break the field splitting, won't break on more complex inputs like $three, and ideally don't rely on external commands. I also prefer not screwing with the IFS, but I don't think that's in the cards.

Reproduction script:
#!/usr/bin/env bash
set -x

# declare 2 vars and export them
function export_locals(){
    local one=1
    local two=$(echo word1 word2)
    local three=$(echo "word1 ain't word2")
    local # aka local -p
}

# insert vars into different local namespace
function import_locals(){
    echo ${one:-nah} ${two:-nope} # nothing up my sleeve
    local $(export_locals)
    local
    echo ${one:-nah} ${two:-nope} ${three:-nope}
}

function test_export_import_locals(){
    export_locals # just to show what it prints
    import_locals # bad attempt
    IFS=$'\n' import_locals # good (ish?) attempt
}
abathur
  • 1,047
  • 7
  • 19
  • Don't try to pass around shell syntax in variables or or function output; the shell expands things like `$var` and `$(command)` partway through the parsing process, so the results get sort of weirdly half-parsed. Depending on what you're actually trying to accomplish, there's almost certain to be a better way to do it. See [BashFAQ #50: I'm trying to put a command in a variable, but the complex cases always fail!](https://mywiki.wooledge.org/BashFAQ/050) – Gordon Davisson May 07 '19 at 03:10

1 Answers1

0

Note: I'm leaving the original answer below unchanged, but the underlying behavior changes in bash 5.1, where the output of local now behaves more like other setters.


I eventually iterated into a tolerable solution with external command xargs, which I've supplied below.

function import_locals(){
    local $(export_locals_xargs)
    local
}
function export_locals_xargs(){
    local one=1
    local two=$(echo word1 word2)
    local three=$(echo "word1 ain't word2")
    local | xargs printf "%s\n"
}
IFS=$'\n' import_locals

This will output:

one=1
three='word1 ain'\''t word2'
two='word1 word2'

Echoing the individual variables outputs:

one: 1
two: word1 word2
three: word1 ain't word2

bash 5.1+

function import_locals(){
    eval "$(export_locals_xargs)"
    local
}
function export_locals_xargs(){
    local one=1
    local two=$(echo word1 word2)
    local three=$(echo "word1 ain't word2")
    local -p
}
import_locals

This outputs:

declare -- one="1"
declare -- three="word1 ain't word2"
declare -- two="word1 word2"
abathur
  • 1,047
  • 7
  • 19