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 refactored version of the code, that includes @thatotherguy pointers:

    #!/bin/sh
    
    declare_p() {
        eval "$(awk '
            BEGIN {
                printf "set --"
                for (i = 1; i < ARGC; i++) {
                    if (ARGV[i] ~ /^[[:alpha:]_][[:alnum:]_]*$/) {
                        printf(" %s \"$%s\"", ARGV[i], ARGV[i])
                    } else {
                        print("declare_p: "ARGV[i]": not a valid identifier") | "cat 1>&2"
                        status = 1
                    }
                }
                print ""
                exit status
            }
        ' "$@" || echo false)"
    
        awk -v status="$?" '
            BEGIN {
                for (i = 1; i < ARGC; i += 2) {
                    gsub(/\047/, "&\\\\&&", ARGV[i+1])
                    print ARGV[i] "=" "\047" ARGV[i+1] "\047"
                }
                exit status
            }
        ' "$@"
    }
    

    Explanations

    1. The first awk builds a command string to redefine the arguments passed to the function; for eg., given the arguments varname1, varname2, etc..., the outputted string will be

      set -- varname1 "$varname1" varname2 "$varname2" ...
      

      When one of the arguments isn't a valid varname (for eg. some%^@junk), the exit status of the first awk command is set to 1. That will trigger the outputting of an additional false line that sets the exit status of the eval command to "erroneous".

    2. Then the second awk builds the shell commands needed for defining the variables, something like:

      varname1='some value'
      varname2='some other value'
      ...
      

      It also "forwards" the exit status of the previous eval command, so that you can test declare_p with a conditional.


    Example

    var1="123 456 789"
    var2=" abc "
    var3="line
    feed"
    
    {
        declare_p var1 var2 var3 || {
            echo 'oops. there was an error' 1>&2
            exit 1
        }
        cat <<-'EOF' # a quoted heredoc doesn't expand anything
            printf '<<%s>>\n' "$var1" "$var2" "$var3"
        EOF
    } |
    ssh user@host sh
    
    <<123 456 789>>
    << abc >>
    <<line
    feed>>