bashsh

POSIX shell equivalent of bash "declare -p"


I'm writing a POSIX shell function based on bash declare -p behaviour, the difference being that it targets POSIX shells.

I've set a few goals:

  1. Make a robust function
    => OK for the current code (I think).
  2. Don't define any variable (to avoid clashes)
    => OK, the current implementation doesn't define any variable
  3. As little as possible forks
    => Currently 2 forks. A single one would be great, but I'm not sure that it is possible without defining any variable.
  4. A return status of 1 when an invalid variable name is given as argument
    => That's not currently the case The current behavior is to process the good arguments and to skip the invalid ones (printing an error message and setting the return status to 1), just like declare -p does.

My current problem is in #4 but any advice about the other points is welcome ;-)

Here's the code. The first awk validates the arguments and generates a shell command for updating $@, which is then processed with eval. The second awk will then print the variables declarations with single-quote escaping:

edit: fixed the code based on @thatotherguy suggestion

#!/bin/sh

declare_p() {
    eval "$(
        awk -v funcname="declare_p" '
            BEGIN {
                output = "set --"
                for (i = 2; i < ARGC; i++) {
                    if (ARGV[i] ~ /^[[:alpha:]_][[:alnum:]_]*$/) {
                        output = output" "ARGV[i]" \042\044"ARGV[i]"\042"
                    } else {
                        print funcname": "ARGV[i]": not a valid identifier" > "/dev/stderr"
                        error = 1
                        # exit 1
                    }
                }
                print output
                exit
            }
            END { if (error) print "false" }
        ' -- "$@"
    )" # || return 1
    awk -v rc="$?" '
        BEGIN {
            for (i = 2; i < ARGC; i += 2) {
                gsub(/\047/,"\047\134\047\047",ARGV[i+1])
                print ARGV[i] "=" "\047" ARGV[i+1] "\047"
            }
            exit rc
        }
    ' -- "$@"
}
Example
#!/bin/sh

var1=" a  b "
var2="a'b"
var3="a
b"
var4="
"

declare_p var1 var2 var3 var4 var-x
echo "return code: $?"

output:

declare_p: var-x: not a valid identifier # (stderr)
var1=' a  b '
var2='a'\''b'
var3='a
b'
var4='
'
return code: 1

Solution

  • Here's a corrected version of the code, based on @thatotherguy advice:

    #!/bin/sh
    
    declare_p() {
        eval "$(
            awk -v funcname="declare_p" '
                function perror(str) { print str | "cat 1>&2" }
                BEGIN {
                    output = "set --"
                    for (i = 2; i < ARGC; i++) {
                        if (ARGV[i] ~ /^[[:alpha:]_][[:alnum:]_]*$/) {
                            output = output " " ARGV[i] " \042\044" ARGV[i] "\042"
                        } else {
                            perror(funcname ": " ARGV[i] ": not a valid identifier")
                            error = 1
                            # exit 1
                        }
                    }
                    print output
                    exit
                }
                END { if (error) print "false" }
            ' -- "$@"
        )" # || return 1
        awk -v rc="$?" '
            BEGIN {
                for (i = 2; i < ARGC; i += 2) {
                    gsub(/\047/,"\047\134\047\047",ARGV[i+1])
                    print ARGV[i] "=" "\047" ARGV[i+1] "\047"
                }
                exit rc
            }
        ' -- "$@"
    }
    

    The first awk forces the eval to end with a false command in case of an invalid parameter; the return code is passed to the second awk with -v rc="$?"


    Example of use-case:
    var1="123 456 789"
    var2=" abc "
    
    {
    declare_p var1 var2
    cat <<'EOF' # quoted heredocs don't expand anything so you can write your raw commands here
    printf '<%s>\n' "$var1" "$var2"
    EOF
    } | ssh user@host sh
    
    <123 456 789>
    < abc >