How to return an array in bash without using globals?

后端 未结 18 2010
Happy的楠姐
Happy的楠姐 2020-11-29 21:36

I have a function that creates an array and I want to return the array to the caller:

create_array() {
  local my_list=(\"a\", \"b\", \"c\")
  echo \"${my_li         


        
相关标签:
18条回答
  • 2020-11-29 21:53

    The easest way y found

    my_function()
    {
        array=(one two three)
        echo ${array[@]}
    }
    
    result=($(my_function))
    
    echo ${result[0]}
    echo ${result[1]}
    echo ${result[2]}
    
    0 讨论(0)
  • 2020-11-29 21:54

    You were not so far out with your original solution. You had a couple of problems, you used a comma as a separator, and you failed to capture the returned items into a list, try this:

    my_algorithm() {
      local result=( $(create_array) )
    }
    
    create_array() {
      local my_list=("a" "b" "c")  
      echo "${my_list[@]}" 
    }
    

    Considering the comments about embedded spaces, a few tweaks using IFS can solve that:

    my_algorithm() {
      oldIFS="$IFS"
      IFS=','
      local result=( $(create_array) )
      IFS="$oldIFS"
      echo "Should be 'c d': ${result[1]}"
    }
    
    create_array() {
      IFS=','
      local my_list=("a b" "c d" "e f") 
      echo "${my_list[*]}" 
    }
    
    0 讨论(0)
  • 2020-11-29 21:54

    A pure bash, minimal and robust solution based on the 'declare -p' builtin — without insane global variables

    This approach involves the following three steps:

    1. Convert the array with 'declare -p' and save the output in a variable.
      myVar="$( declare -p myArray )"
      The output of the declare -p statement can be used to recreate the array. For instance the output of declare -p myVar might look like this:
      declare -a myVar='([0]="1st field" [1]="2nd field" [2]="3rd field")'
    2. Use the echo builtin to pass the variable to a function or to pass it back from there.
      • In order to preserve whitspaces in array fields when echoing the variable, IFS is temporarly set to a control character (e.g. a vertical tab).
      • Only the right-hand-side of the declare statement in the variable is to be echoed - this can be achieved by parameter expansion of the form ${parameter#word}. As for the example above: ${myVar#*=}
    3. Finally, recreate the array where it is passed to using the eval and the 'declare -a' builtins.

    Example 1 - return an array from a function

    #!/bin/bash
    
    # Example 1 - return an array from a function
    
    function my-fun () {
     # set up a new array with 3 fields - note the whitespaces in the
     # 2nd (2 spaces) and 3rd (2 tabs) field
     local myFunArray=( "1st field" "2nd  field" "3rd       field" )
    
     # show its contents on stderr (must not be output to stdout!)
     echo "now in $FUNCNAME () - showing contents of myFunArray" >&2
     echo "by the help of the 'declare -p' builtin:" >&2
     declare -p myFunArray >&2
    
     # return the array
     local myVar="$( declare -p myFunArray )"
     local IFS=$'\v';
     echo "${myVar#*=}"
    
     # if the function would continue at this point, then IFS should be
     # restored to its default value: <space><tab><newline>
     IFS=' '$'\t'$'\n';
    }
    
    # main
    
    # call the function and recreate the array that was originally
    # set up in the function
    eval declare -a myMainArray="$( my-fun )"
    
    # show the array contents
    echo ""
    echo "now in main part of the script - showing contents of myMainArray"
    echo "by the help of the 'declare -p' builtin:"
    declare -p myMainArray
    
    # end-of-file
    

    Output of Example 1:

    now in my-fun () - showing contents of myFunArray
    by the help of the 'declare -p' builtin:
    declare -a myFunArray='([0]="1st field" [1]="2nd  field" [2]="3rd       field")'
    
    now in main part of the script - showing contents of myMainArray
    by the help of the 'declare -p' builtin:
    declare -a myMainArray='([0]="1st field" [1]="2nd  field" [2]="3rd      field")'
    

    Example 2 - pass an array to a function

    #!/bin/bash
    
    # Example 2 - pass an array to a function
    
    function my-fun () {
     # recreate the array that was originally set up in the main part of
     # the script
     eval declare -a myFunArray="$( echo "$1" )"
    
     # note that myFunArray is local - from the bash(1) man page: when used
     # in a function, declare makes each name local, as with the local
     # command, unless the ‘-g’ option is used.
    
     # IFS has been changed in the main part of this script - now that we
     # have recreated the array it's better to restore it to the its (local)
     # default value: <space><tab><newline>
     local IFS=' '$'\t'$'\n';
    
     # show contents of the array
     echo ""
     echo "now in $FUNCNAME () - showing contents of myFunArray"
     echo "by the help of the 'declare -p' builtin:"
     declare -p myFunArray
    }
    
    # main
    
    # set up a new array with 3 fields - note the whitespaces in the
    # 2nd (2 spaces) and 3rd (2 tabs) field
    myMainArray=( "1st field" "2nd  field" "3rd     field" )
    
    # show the array contents
    echo "now in the main part of the script - showing contents of myMainArray"
    echo "by the help of the 'declare -p' builtin:"
    declare -p myMainArray
    
    # call the function and pass the array to it
    myVar="$( declare -p myMainArray )"
    IFS=$'\v';
    my-fun $( echo "${myVar#*=}" )
    
    # if the script would continue at this point, then IFS should be restored
    # to its default value: <space><tab><newline>
    IFS=' '$'\t'$'\n';
    
    # end-of-file
    

    Output of Example 2:

    now in the main part of the script - showing contents of myMainArray
    by the help of the 'declare -p' builtin:
    declare -a myMainArray='([0]="1st field" [1]="2nd  field" [2]="3rd      field")'
    
    now in my-fun () - showing contents of myFunArray
    by the help of the 'declare -p' builtin:
    declare -a myFunArray='([0]="1st field" [1]="2nd  field" [2]="3rd       field")'
    
    0 讨论(0)
  • 2020-11-29 21:54

    If your source data is formatted with each list element on a separate line, then the mapfile builtin is a simple and elegant way to read a list into an array:

    $ list=$(ls -1 /usr/local)           # one item per line
    
    $ mapfile -t arrayVar <<<"$list"     # -t trims trailing newlines
    
    $ declare -p arrayVar | sed 's#\[#\n[#g'
    declare -a arrayVar='(
    [0]="bin"
    [1]="etc"
    [2]="games"
    [3]="include"
    [4]="lib"
    [5]="man"
    [6]="sbin"
    [7]="share"
    [8]="src")'
    

    Note that, as with the read builtin, you would not ordinarily* use mapfile in a pipeline (or subshell) because the assigned array variable would be unavailable to subsequent statements (* unless bash job control is disabled and shopt -s lastpipe is set).

    $ help mapfile
    mapfile: mapfile [-n count] [-O origin] [-s count] [-t] [-u fd] [-C callback] [-c quantum] [array]
        Read lines from the standard input into an indexed array variable.
    
        Read lines from the standard input into the indexed array variable ARRAY, or
        from file descriptor FD if the -u option is supplied.  The variable MAPFILE
        is the default ARRAY.
    
        Options:
          -n count  Copy at most COUNT lines.  If COUNT is 0, all lines are copied.
          -O origin Begin assigning to ARRAY at index ORIGIN.  The default index is 0.
          -s count  Discard the first COUNT lines read.
          -t                Remove a trailing newline from each line read.
          -u fd             Read lines from file descriptor FD instead of the standard input.
          -C callback       Evaluate CALLBACK each time QUANTUM lines are read.
          -c quantum        Specify the number of lines read between each call to CALLBACK.
    
        Arguments:
          ARRAY             Array variable name to use for file data.
    
        If -C is supplied without -c, the default quantum is 5000.  When
        CALLBACK is evaluated, it is supplied the index of the next array
        element to be assigned and the line to be assigned to that element
        as additional arguments.
    
        If not supplied with an explicit origin, mapfile will clear ARRAY before
        assigning to it.
    
        Exit Status:
        Returns success unless an invalid option is given or ARRAY is readonly or
        not an indexed array.
    
    0 讨论(0)
  • 2020-11-29 21:55

    [Note: the following was rejected as an edit of this answer for reasons that make no sense to me (since the edit was not intended to address the author of the post!), so I'm taking the suggestion to make it a separate answer.]

    A simpler implementation of Steve Zobell's adaptation of Matt McClure's technique uses the bash built-in (since version == 4) readarray as suggested by RastaMatt to create a representation of an array that can be converted into an array at runtime. (Note that both readarray and mapfile name the same code.) It still avoids globals (allowing use of the function in a pipe), and still handles nasty characters.

    For some more-fully-developed (e.g., more modularization) but still-kinda-toy examples, see bash_pass_arrays_between_functions. Following are a few easily-executable examples, provided here to avoid moderators b!tching about external links.

    Cut the following block and paste it into a bash terminal to create /tmp/source.sh and /tmp/junk1.sh:

    FP='/tmp/source.sh'     # path to file to be created for `source`ing
    cat << 'EOF' > "${FP}"  # suppress interpretation of variables in heredoc
    function make_junk {
       echo 'this is junk'
       echo '#more junk and "b@d" characters!'
       echo '!#$^%^&(*)_^&% ^$#@:"<>?/.,\\"'"'"
    }
    
    ### Use 'readarray' (aka 'mapfile', bash built-in) to read lines into an array.
    ### Handles blank lines, whitespace and even nastier characters.
    function lines_to_array_representation {
        local -a arr=()
        readarray -t arr
        # output array as string using 'declare's representation (minus header)
        declare -p arr | sed -e 's/^declare -a [^=]*=//'
    }
    EOF
    
    FP1='/tmp/junk1.sh'      # path to script to run
    cat << 'EOF' > "${FP1}"  # suppress interpretation of variables in heredoc
    #!/usr/bin/env bash
    
    source '/tmp/source.sh'  # to reuse its functions
    
    returned_string="$(make_junk | lines_to_array_representation)"
    eval "declare -a returned_array=${returned_string}"
    for elem in "${returned_array[@]}" ; do
        echo "${elem}"
    done
    EOF
    chmod u+x "${FP1}"
    # newline here ... just hit Enter ...
    

    Run /tmp/junk1.sh: output should be

    this is junk
    #more junk and "b@d" characters!
    !#$^%^&(*)_^&% ^$#@:"<>?/.,\\"'
    

    Note lines_to_array_representation also handles blank lines. Try pasting the following block into your bash terminal:

    FP2='/tmp/junk2.sh'      # path to script to run
    cat << 'EOF' > "${FP2}"  # suppress interpretation of variables in heredoc
    #!/usr/bin/env bash
    
    source '/tmp/source.sh'  # to reuse its functions
    
    echo '`bash --version` the normal way:'
    echo '--------------------------------'
    bash --version
    echo # newline
    
    echo '`bash --version` via `lines_to_array_representation`:'
    echo '-----------------------------------------------------'
    bash_version="$(bash --version | lines_to_array_representation)"
    eval "declare -a returned_array=${bash_version}"
    for elem in "${returned_array[@]}" ; do
        echo "${elem}"
    done
    echo # newline
    
    echo 'But are they *really* the same? Ask `diff`:'
    echo '-------------------------------------------'
    
    echo 'You already know how to capture normal output (from `bash --version`):'
    declare -r PATH_TO_NORMAL_OUTPUT="$(mktemp)"
    bash --version > "${PATH_TO_NORMAL_OUTPUT}"
    echo "normal output captured to file @ ${PATH_TO_NORMAL_OUTPUT}"
    ls -al "${PATH_TO_NORMAL_OUTPUT}"
    echo # newline
    
    echo 'Capturing L2AR takes a bit more work, but is not onerous.'
    echo "Look @ contents of the file you're about to run to see how it's done."
    
    declare -r RAW_L2AR_OUTPUT="$(bash --version | lines_to_array_representation)"
    declare -r PATH_TO_COOKED_L2AR_OUTPUT="$(mktemp)"
    eval "declare -a returned_array=${RAW_L2AR_OUTPUT}"
    for elem in "${returned_array[@]}" ; do
        echo "${elem}" >> "${PATH_TO_COOKED_L2AR_OUTPUT}"
    done
    echo "output from lines_to_array_representation captured to file @ ${PATH_TO_COOKED_L2AR_OUTPUT}"
    ls -al "${PATH_TO_COOKED_L2AR_OUTPUT}"
    echo # newline
    
    echo 'So are they really the same? Per'
    echo "\`diff -uwB "${PATH_TO_NORMAL_OUTPUT}" "${PATH_TO_COOKED_L2AR_OUTPUT}" | wc -l\`"
    diff -uwB "${PATH_TO_NORMAL_OUTPUT}" "${PATH_TO_COOKED_L2AR_OUTPUT}" | wc -l
    echo '... they are the same!'
    EOF
    chmod u+x "${FP2}"
    # newline here ... just hit Enter ...
    

    Run /tmp/junk2.sh @ commandline. Your output should be similar to mine:

    `bash --version` the normal way:
    --------------------------------
    GNU bash, version 4.3.30(1)-release (x86_64-pc-linux-gnu)
    Copyright (C) 2013 Free Software Foundation, Inc.
    License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
    
    This is free software; you are free to change and redistribute it.
    There is NO WARRANTY, to the extent permitted by law.
    
    `bash --version` via `lines_to_array_representation`:
    -----------------------------------------------------
    GNU bash, version 4.3.30(1)-release (x86_64-pc-linux-gnu)
    Copyright (C) 2013 Free Software Foundation, Inc.
    License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
    
    This is free software; you are free to change and redistribute it.
    There is NO WARRANTY, to the extent permitted by law.
    
    But are they *really* the same? Ask `diff`:
    -------------------------------------------
    You already know how to capture normal output (from `bash --version`):
    normal output captured to file @ /tmp/tmp.Ni1bgyPPEw
    -rw------- 1 me me 308 Jun 18 16:27 /tmp/tmp.Ni1bgyPPEw
    
    Capturing L2AR takes a bit more work, but is not onerous.
    Look @ contents of the file you're about to run to see how it's done.
    output from lines_to_array_representation captured to file @ /tmp/tmp.1D6O2vckGz
    -rw------- 1 me me 308 Jun 18 16:27 /tmp/tmp.1D6O2vckGz
    
    So are they really the same? Per
    `diff -uwB /tmp/tmp.Ni1bgyPPEw /tmp/tmp.1D6O2vckGz | wc -l`
    0
    ... they are the same!
    
    0 讨论(0)
  • 2020-11-29 21:56

    Here is a solution with no external array references and no IFS manipulation:

    # add one level of single quotes to args, eval to remove
    squote () {
        local a=("$@")
        a=("${a[@]//\'/\'\\\'\'}")   # "'" => "'\''"
        a=("${a[@]/#/\'}")           # add "'" prefix to each word
        a=("${a[@]/%/\'}")           # add "'" suffix to each word
        echo "${a[@]}"
    }
    
    create_array () {
        local my_list=(a "b 'c'" "\\\"d
    ")
        squote "${my_list[@]}"
    }
    
    my_algorithm () {
        eval "local result=($(create_array))"
        # result=([0]="a" [1]="b 'c'" [2]=$'\\"d\n')
    }
    
    0 讨论(0)
提交回复
热议问题