如何从Bash函数返回字符串值

How to return a string value from a Bash function

我想从Bash函数返回一个字符串。

我将在java中编写示例以显示我想要做的事情:

1
2
3
4
5
public String getSomeString() {
  return"tadaa";
}

String variable = getSomeString();

以下示例适用于bash,但有更好的方法吗?

1
2
3
4
5
function getSomeString {
   echo"tadaa"
}

VARIABLE=$(getSomeString)


我知道没有更好的方法。 Bash只知道写入stdout的状态代码(整数)和字符串。


您可以让函数将变量作为第一个arg,并使用要返回的字符串修改变量。

1
2
3
4
5
6
7
8
9
#!/bin/bash
set -x
function pass_back_a_string() {
    eval"$1='foo bar rab oof'"
}

return_var=''
pass_back_a_string return_var
echo $return_var

打印"foo bar rab oof"。

编辑:在适当的地方添加引用,以允许字符串中的空格来解决@Luca Borrione的评论。

编辑:作为演示,请参阅以下程序。这是一个通用解决方案:它甚至允许您将字符串接收到局部变量中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#!/bin/bash
set -x
function pass_back_a_string() {
    eval"$1='foo bar rab oof'"
}

return_var=''
pass_back_a_string return_var
echo $return_var

function call_a_string_func() {
     local lvar=''
     pass_back_a_string lvar
     echo"lvar='$lvar' locally"
}

call_a_string_func
echo"lvar='$lvar' globally"

这打印:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
+ return_var=
+ pass_back_a_string return_var
+ eval 'return_var='\''foo bar rab oof'\'''
++ return_var='foo bar rab oof'
+ echo foo bar rab oof
foo bar rab oof
+ call_a_string_func
+ local lvar=
+ pass_back_a_string lvar
+ eval 'lvar='\''foo bar rab oof'\'''
++ lvar='foo bar rab oof'
+ echo 'lvar='\''foo bar rab oof'\'' locally'
lvar='foo bar rab oof' locally
+ echo 'lvar='\'''\'' globally'
lvar='' globally

编辑:证明原始变量的值在函数中可用,正如@Xichen Li在评论中错误批评的那样。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#!/bin/bash
set -x
function pass_back_a_string() {
    eval"echo in pass_back_a_string, original $1 is \$$1"
    eval"$1='foo bar rab oof'"
}

return_var='original return_var'
pass_back_a_string return_var
echo $return_var

function call_a_string_func() {
     local lvar='original lvar'
     pass_back_a_string lvar
     echo"lvar='$lvar' locally"
}

call_a_string_func
echo"lvar='$lvar' globally"

这给出了输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
+ return_var='original return_var'
+ pass_back_a_string return_var
+ eval 'echo in pass_back_a_string, original return_var is $return_var'
++ echo in pass_back_a_string, original return_var is original return_var
in pass_back_a_string, original return_var is original return_var
+ eval 'return_var='\''foo bar rab oof'\'''
++ return_var='foo bar rab oof'
+ echo foo bar rab oof
foo bar rab oof
+ call_a_string_func
+ local 'lvar=original lvar'
+ pass_back_a_string lvar
+ eval 'echo in pass_back_a_string, original lvar is $lvar'
++ echo in pass_back_a_string, original lvar is original lvar
in pass_back_a_string, original lvar is original lvar
+ eval 'lvar='\''foo bar rab oof'\'''
++ lvar='foo bar rab oof'
+ echo 'lvar='\''foo bar rab oof'\'' locally'
lvar='foo bar rab oof' locally
+ echo 'lvar='\'''\'' globally'
lvar='' globally


上面的所有答案都忽略了bash手册页中所述的内容。

  • 函数内声明的所有变量都将与调用环境共享。
  • 声明为local的所有变量都不会被共享。

示例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#!/bin/bash

f()
{
    echo function starts
    local WillNotExists="It still does!"
    DoesNotExists="It still does!"
    echo function ends
}

echo $DoesNotExists #Should print empty line
echo $WillNotExists #Should print empty line
f                   #Call the function
echo $DoesNotExists #Should print It still does!
echo $WillNotExists #Should print empty line

并输出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$ sh -x ./x.sh
+ echo

+ echo

+ f
+ echo function starts
function starts
+ local 'WillNotExists=It still does!'
+ DoesNotExists='It still does!'
+ echo function ends
function ends
+ echo It still 'does!'
It still does!
+ echo

同样在pdksh和ksh下这个脚本也是一样的!


Bash,自版本4.3,2014年2月(?),明确支持引用变量或名称引用(namerefs),超出"eval",具有相同的有益性能和间接效果,并且在脚本中可能更清晰,也更难"忘记'eval'并且必须修复此错误":

1
2
3
4
5
6
7
8
9
10
11
12
13
declare [-aAfFgilnrtux] [-p] [name[=value] ...]
typeset [-aAfFgilnrtux] [-p] [name[=value] ...]
  Declare variables and/or give them attributes
  ...
  -n Give each name the nameref attribute, making it a name reference
     to another variable.  That other variable is defined by the value
     of name.  All references and assignments to name, except for?
     changing the -n attribute itself, are performed on the variable
     referenced by name's value.  The -n attribute cannot be applied to
     array variables.
...
When used in a function, declare and typeset make each name local,
as with the local command, unless the -g option is supplied...

并且:

PARAMETERS

A variable can be assigned the nameref attribute using the -n option to the
declare or local builtin commands (see the descriptions of declare and local
below) to create a nameref, or a reference to another variable. This allows
variables to be manipulated indirectly. Whenever the nameref variable is?
referenced or assigned to, the operation is actually performed on the variable
specified by the nameref variable's value. A nameref is commonly used within
shell functions to refer to a variable whose name is passed as an argument to?
the function. For instance, if a variable name is passed to a shell function
as its first argument, running

1
      declare -n ref=$1

inside the function creates a nameref variable ref whose value is the variable
name passed as the first argument. References and assignments to ref are
treated as references and assignments to the variable whose name was passed as?
$1. If the control variable in a for loop has the nameref attribute, the list
of words can be a list of shell variables, and a name reference will be?
established for each word in the list, in turn, when the loop is executed.
Array variables cannot be given the -n attribute. However, nameref variables
can reference array variables and subscripted array variables. Namerefs can be?
unset using the -n option to the unset builtin. Otherwise, if unset is executed
with the name of a nameref variable as an argument, the variable referenced by?
the nameref variable will be unset.

例如(编辑2 :(谢谢Ron)命名空间(前缀)函数内部变量名称,以最小化外部变量冲突,最终应该正确回答,Karsten在评论中提出的问题):

1
2
3
4
5
6
7
# $1 : string; your variable to contain the return value
function return_a_string () {
    declare -n ret=$1
    local MYLIB_return_a_string_message="The date is"
    MYLIB_return_a_string_message+=$(date)
    ret=$MYLIB_return_a_string_message
}

并测试此示例:

1
2
$ return_a_string result; echo $result
The date is 20160817

请注意,bash"declare"builtin在函数中使用时,默认使声明变量为"local"," - n"也可以与"local"一起使用。

我更喜欢将"重要声明"变量与"无聊的本地"变量区分开来,因此以这种方式使用"declare"和"local"作为文档。

编辑1 - (回应Karsten的评论) - 我不能再在下面添加评论了,但是Karsten的评论让我思考,所以我做了以下测试,哪个工作很精细,AFAICT - Karsten如果你读到这个,请提供一套确切的设置来自命令行的测试步骤,显示您认为存在的问题,因为以下步骤可以正常工作:

1
2
$ return_a_string ret; echo $ret
The date is 20170104

(在将上述函数粘贴到bash术语之后,我刚刚运行了这个 - 正如您所看到的,结果运行得很好。)


像上面的bstpierre一样,我使用并建议使用显式命名输出变量:

1
2
3
4
5
6
7
function some_func() # OUTVAR ARG1
{
   local _outvar=$1
   local _result # Use some naming convention to avoid OUTVARs to clash
   ... some processing ....
   eval $_outvar=\$_result # Instead of just =$_result
}

注意使用引用$。这将避免将$result中的内容解释为shell特殊字符。我发现这比捕获回声的result=$(some_func"arg1")成语快一个数量级。使用MSYS上的bash速度差异显得更加显着,其中从函数调用捕获的stdout几乎是灾难性的。

发送局部变量是可以的,因为本地变量在bash中是动态范围的:

1
2
3
4
5
6
function another_func() # ARG
{
   local result
   some_func result"$1"
   echo result is $result
}


您还可以捕获函数输出:

1
2
3
4
5
6
7
8
9
10
#!/bin/bash
function getSomeString() {
     echo"tadaa!"
}

return_var=$(getSomeString)
echo $return_var
# Alternative syntax:
return_var=`getSomeString`
echo $return_var

看起来很奇怪,但比使用全局变量恕我直言更好。传递参数像往常一样工作,只需将它们放在大括号或反引号中。


如前所述,从函数返回字符串的"正确"方法是使用命令替换。如果函数还需要输出到控制台(如上面提到的@Mani),请在函数的开头创建一个临时fd并重定向到控制台。在返回字符串之前关闭临时fd。

1
2
3
4
5
6
7
8
9
10
11
12
13
#!/bin/bash
# file:  func_return_test.sh
returnString() {
    exec 3>&1 >/dev/tty
    local s=$1
    s=${s:="some default string"}
    echo"writing directly to console"
    exec 3>&-    
    echo"$s"
}

my_string=$(returnString"$*")
echo"my_string:  [$my_string]"

执行没有参数的脚本会产生......

1
2
3
# ./func_return_test.sh
writing directly to console
my_string:  [some default string]

希望这有助于人们

-Andy


其他人写道:最直接,最强大的解决方案是使用命令替换。

1
2
3
4
5
6
7
8
assign()
{
    local x
    x="Test"
    echo"$x"
}

x=$(assign) # This assigns string"Test" to x

缺点是性能,因为这需要一个单独的过程。

本主题中提出的另一种技术,即传递一个变量的名称作为参数赋值,有副作用,我不推荐它的基本形式。问题是您可能需要函数中的一些变量来计算返回值,并且可能会发生用于存储返回值的变量的名称将干扰其中一个:

1
2
3
4
5
6
7
8
9
10
11
assign()
{
    local x
    x="Test"
    eval"$1=\$x"
}

assign y # This assigns string"Test" to y, as expected

assign x # This will NOT assign anything to x in this scope
         # because the name"x" is declared as local inside the function

当然,您可能不会将函数的内部变量声明为本地变量,但实际上您应该始终这样做,否则,如果存在具有相同名称的变量,您可能会意外地覆盖父作用域中的不相关变量。 。

一种可能的解决方法是将传递的变量显式声明为全局变量:

1
2
3
4
5
6
7
assign()
{
    local x
    eval declare -g $1
    x="Test"
    eval"$1=\$x"
}

如果名称"x"作为参数传递,则函数体的第二行将覆盖先前的本地声明。但是名称本身可能仍会干扰,因此如果您打算在将返回值写入之前使用先前存储在传递变量中的值,请注意必须在最开始时将其复制到另一个局部变量中;否则结果将无法预测!
此外,这只适用于最新版本的BASH,即4.2。更多可移植代码可能会使用具有相同效果的显式条件结构:

1
2
3
4
5
6
7
8
assign()
{
    if [[ $1 != x ]]; then
      local x
    fi
    x="Test"
    eval"$1=\$x"
}

也许最优雅的解决方案就是为函数返回值保留一个全局名称
在您编写的每个函数中始终使用它。


您可以使用全局变量:

1
2
3
4
5
6
7
8
9
10
declare globalvar='some string'

string ()
{
  eval "$1='some other string'"
} # ----------  end of function string  ----------

string globalvar

echo"'${globalvar}'"

这给了

1
'some other string'

为了说明我对Andy的答案的评论,使用额外的文件描述符操作来避免使用/dev/tty

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#!/bin/bash

exec 3>&1

returnString() {
    exec 4>&1 >&3
    local s=$1
    s=${s:="some default string"}
    echo"writing to stdout"
    echo"writing to stderr">&2
    exec >&4-
    echo"$s"
}

my_string=$(returnString"$*")
echo"my_string:  [$my_string]"

但仍然很讨厌。


考虑到以下代码,请向Vicky Ronnen致敬:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
function use_global
{
    eval"$1='changed using a global var'"
}

function capture_output
{
    echo"always changed"
}

function test_inside_a_func
{
    local _myvar='local starting value'
    echo"3. $_myvar"

    use_global '_myvar'
    echo"4. $_myvar"

    _myvar=$( capture_output )
    echo"5. $_myvar"
}

function only_difference
{
    local _myvar='local starting value'
    echo"7. $_myvar"

    local use_global '_myvar'
    echo"8. $_myvar"

    local _myvar=$( capture_output )
    echo"9. $_myvar"
}

declare myvar='global starting value'
echo"0. $myvar"

use_global 'myvar'
echo"1. $myvar"

myvar=$( capture_output )
echo"2. $myvar"

test_inside_a_func
echo"6. $_myvar" # this was local inside the above function

only_difference

会给

1
2
3
4
5
6
7
8
9
10
0. global starting value
1. changed using a global var
2. always changed
3. local starting value
4. changed using a global var
5. always changed
6.
7. local starting value
8. local starting value
9. always changed

也许正常的情况是使用test_inside_a_func函数中使用的语法,因此在大多数情况下你可以使用这两种方法,虽然捕获输出是更安全的方法总是在任何情况下工作,模仿函数的返回值您可以在其他语言中找到,正如Vicky Ronnen正确指出的那样。


您拥有它的方式是在不破坏范围的情况下实现此目的的唯一方法。 Bash没有返回类型的概念,只有退出代码和文件描述符(stdin / out / err等)


我认为,所有选项都已列举。选择一个可能归结为特定应用的最佳风格问题,在这种情况下,我想提供一种我发现有用的特定风格。在bash中,变量和函数不在同一名称空间中。因此,处理与函数值相同名称的变量是一种惯例,我发现如果我严格应用它,可以最大限度地减少名称冲突并增强可读性。现实生活中的一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
UnGetChar=
function GetChar() {
    # assume failure
    GetChar=
    # if someone previously"ungot" a char
    if ! [ -z"$UnGetChar" ]; then
        GetChar="$UnGetChar"
        UnGetChar=
        return 0               # success
    # else, if not at EOF
    elif IFS= read -N1 GetChar ; then
        return 0           # success
    else
        return 1           # EOF
    fi
}

function UnGetChar(){
    UnGetChar="$1"
}

并且,使用这些功能的示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function GetToken() {
    # assume failure
    GetToken=
    # if at end of file
    if ! GetChar; then
        return 1              # EOF
    # if start of comment
    elif [["$GetChar" =="#" ]]; then
        while [["$GetChar" != $'
'
]]; do
            GetToken+="$GetChar"
            GetChar
        done
        UnGetChar"$GetChar"
    # if start of quoted string
    elif ["$GetChar" == '"' ]; then
# ... et cetera

如您所见,返回状态可供您在需要时使用,如果不需要则可忽略。同样可以使用或忽略"返回"变量,但当然只有在调用函数之后。

当然,这只是一个惯例。您可以自由地在返回之前设置关联值(因此我的约定总是在函数的开头将其置零)或者通过再次调用函数(可能是间接的)来践踏其值。不过,如果我发现自己大量使用bash函数,这是一个非常有用的约定。

与情绪相反,这是一个标志,例如"转向perl",我的理念是,约定对于管理任何语言的复杂性始终是重要的。


它们是任何"命名输出变量"方案的关键问题,其中调用者可以传入变量名称(无论是使用eval还是declare -n)是无意的别名,即名称冲突:从封装的角度来看,它很糟糕能够在函数中添加或重命名局部变量,而不首先检查所有函数的调用者,以确保他们不想传递与输出参数相同的名称。 (或者在另一个方向,我不想阅读我正在调用的函数的来源,只是为了确保我打算使用的输出参数不是该函数中的本地。)

唯一的方法是使用单个专用输出变量,如REPLY(如Evi1M4chine所建议)或类似Ron Burk建议的约定。

但是,函数可以在内部使用固定的输出变量,然后在顶部添加一些糖来隐藏调用者的这个事实,就像我在下面的例子中使用call函数一样。认为这是一个概念证明,但关键点是

  • 该函数始终将返回值分配给REPLY,并且还可以像往常一样返回退出代码
  • 从调用者的角度来看,返回值可以分配给任何变量(本地或全局),包括REPLY(参见wrapper示例)。函数的退出代码被传递,因此在例如它们中使用它们。 ifwhile或类似结构按预期工作。
  • 从语法上讲,函数调用仍然是一个简单的语句。

这样做的原因是因为call函数本身没有本地,并且不使用除REPLY之外的其他变量,从而避免了任何名称冲突的可能性。在分配调用者定义的输出变量名称时,我们实际上在调用者的范围内(技术上在call函数的相同范围内),而不是在被调用函数的范围内。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#!/bin/bash
function call() { # var=func [args ...]
  REPLY=;"${1#*=}""${@:2}"; eval"${1%%=*}=\$REPLY; return $?"
}

function greet() {
  case"$1" in
    us) REPLY="hello";;
    nz) REPLY="kia ora";;
    *) return 123;;
  esac
}

function wrapper() {
  call REPLY=greet"$@"
}

function main() {
  local a b c d
  call a=greet us
  echo"a='$a' ($?)"
  call b=greet nz
  echo"b='$b' ($?)"
  call c=greet de
  echo"c='$c' ($?)"
  call d=wrapper us
  echo"d='$d' ($?)"
}
main

输出:

1
2
3
4
a='hello' (0)
b='kia ora' (0)
c='' (123)
d='hello' (0)

bash模式返回标量和数组值对象:

定义

1
2
3
4
5
url_parse() { # parse 'url' into: 'url_host', 'url_port', ...
   local"$@" # inject caller 'url' argument in local scope
   local url_host="..." url_path="..." # calculate 'url_*' components
   declare -p ${!url_*} # return only 'url_*' object fields to the caller
}

调用

1
2
3
4
main() { # invoke url parser and inject 'url_*' results in local scope
   eval"$(url_parse url=http://host/path)" # parse 'url'
   echo"host=$url_host path=$url_path" # use 'url_*' components
}

你可以echo一个字符串,但通过管道(|)将该函数捕获到别的东西。

您可以使用expr执行此操作,但ShellCheck会将此用法报告为已弃用。


在我的程序中,按照惯例,这是预先存在的$REPLY变量的用途,read用于该确切目的。

1
2
3
4
5
6
function getSomeString {
  REPLY="tadaa"
}

getSomeString
echo $REPLY

这个echo es

1
tadaa

但是为了避免冲突,任何其他全局变量都可以。

1
2
3
4
5
6
7
8
declare result

function getSomeString {
  result="tadaa"
}

getSomeString
echo $result

如果这还不够,我推荐Markarian451的解决方案。


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
agt@agtsoft:~/temp$ cat ./fc
#!/bin/sh

fcall='function fcall { local res p=$1; shift; fname $*; eval"$p=$res"; }; fcall'

function f1 {
    res=$[($1+$2)*2];
}

function f2 {
    local a;
    eval ${fcall//fname/f1} a 2 3;
    echo f2:$a;
}

a=3;
f2;
echo after:a=$a, res=$res

agt@agtsoft:~/temp$ ./fc
f2:10
after:a=3, res=