如何获取/ bin / sh函数的最后一个参数


11

有什么更好的实施方法print_last_arg

#!/bin/sh

print_last_arg () {
    eval "echo \${$#}"  # this hurts
}

print_last_arg foo bar baz
# baz

(如果#!/usr/bin/zsh不是这样,那么#!/bin/sh我不知道该怎么办。我的问题是找到一种合理的方法来实现#!/bin/sh。)

编辑:上面只是一个愚蠢的例子。我的目标不是打印最后一个参数,而是要有一种方法来引用Shell函数中的最后一个参数。


EDIT2:对于这样一个措辞不清楚的问题,我深表歉意。 我希望这次能做对。

如果/bin/zsh不是/bin/sh,我可以写这样的东西

#!/bin/zsh

print_last_arg () {
    local last_arg=$argv[$#]
    echo $last_arg
}

该表达式$argv[$#]是我在第一次EDIT中描述的一个示例,该引用是引用Shell函数中最后一个参数的方式

因此,我真的应该像这样写我的原始示例:

print_last_arg () {
    local last_arg=$(eval "echo \${$#}")   # but this hurts even more
    echo $last_arg
}

……清楚地说,我要做的只是放在作业右边的一件不太可怕的事情。

但是请注意,在所有示例中,最后一个参数都是无损访问。IOW,最后一个参数的访问使位置参数作为一个整体不受影响。


unix.stackexchange.com/q/145522/117549指出了#!/ bin / sh的各种可能性-您能将其范围缩小吗?
Jeff Schaller

我确实喜欢编辑,但也许您可能已经注意到,这里有一个答案,提供了一种无损手段来引用最后一个参数...?您不应该等同var=$( eval echo \${$#})eval var=\${$#}-两者完全不同。
mikeserv

1
不确定我是否能得到您的最后的笔记,但是到目前为止,几乎所有提供的答案都没有破坏性,因为它们确实保留了正在运行的脚本参数。只有shiftset -- ...除非他们是无害的,太功能使用基础的解决方案可能是破坏性的。
jlliagre '16

@jlliagre-但它们基本上仍然具有破坏性-它们需要创建可处理的上下文,以便可以销毁以进行发现。但是...如果仍然获得第二个上下文-为什么不仅仅获得一个允许您索引的上下文呢?使用适合该工作的工具有什么问题吗?将shell扩展解释为可扩展输入是eval的工作。与eval "var=\${$#}"相比,这样做没有什么明显的区别,var=${arr[evaled index]}只是$#保证的安全值。为什么要复制整个集合然后在直接索引的情况下销毁它呢?
mikeserv '16

1
@mikeserv在外壳的主要部分完成的for循环保留所有参数不变。我同意循环所有参数是未优化的,特别是如果将数千个参数传递给外壳程序,我也同意直接访问具有适当索引的最后一个参数是最佳答案(而且我不明白为什么它被否决了) ),但除此之外,没有任何破坏性的内容,也没有创建额外的上下文。
jlliagre

Answers:


1
eval printf %s${1+"'\n' \"the last arg is \${$#"\}\"}

...将打印字符串,the last arg is后跟一个<space>,最后一个参数的值,以及结尾的<newline>(如果存在至少1个参数),否则,对于零个参数,将不打印任何内容。

如果您这样做:

eval ${1+"lastarg=\${$#"\}}

...然后,$lastarg如果存在至少一个参数,则可以将最后一个参数的值分配给shell变量,或者根本不执行任何操作。无论哪种方式,您都可以安全地进行操作,我认为它甚至可以移植到Olde Bourne外壳上。

这是另一个工作类似的方法,尽管它确实需要复制整个arg数组两次(并且对于Bourne shell 需要printfin $PATH

if   [ "${1+:}" ]
then set "$#" "$@" "$@"
     shift    "$1"
     printf %s\\n "$1"
     shift
fi

评论不作进一步讨论;此对话已转移至聊天
terdon

2
仅供参考,如果没有参数,您的第一个建议在旧版bourne shell下失败,并出现“错误替换”错误。其余两个都按预期工作。
jlliagre

@jillagre-谢谢。我不太确定前一个-但对其他两个很确定。它只是说明如何通过内联引用访问参数。最好是一个函数${1+":"} return马上就会打开,因为它是谁想要它做任何事情或冒着任何副作用的风险,如果什么都不需要的话?数学是一样的-如果您可以确定可以扩展正整数,则可以eval整天使用。$OPTIND非常适合
mikeserv '16

3

这是一种简单的方法:

print_last_arg () {
  if [ "$#" -gt 0 ]
  then
    s=$(( $# - 1 ))
  else
    s=0
  fi
  shift "$s"
  echo "$1"
}

(根据@cuonglm的观点进行了更新,即当不传递任何参数时原始文档会失败;现在会回显空白行— else如果需要,可以更改子句中的行为)


这需要在Solaris上使用/ usr / xpg4 / bin / sh;让我知道是否需要Solaris#!/ bin / sh。
Jeff Schaller

这个问题与我要编写的特定脚本无关,而是与一般操作方法有关。我正在为此寻找好的食谱。当然,便携性越好。
kjo

$(())数学在Solaris / bin / sh中失败;使用类似:shift`expr $#-1`,如果需要的话。
Jeff Schaller

或者对于Solaris / bin / sh,echo "$# - 1" | bc
Jeff Schaller

1
那正是我想要写的,但是在我尝试的第二个shell上失败了-NetBSD,它显然是不支持它的Almquist shell :(
Jeff Schaller

3

给定开头帖子的示例(不带空格的位置参数):

print_last_arg foo bar baz

对于默认值IFS=' \t\n',如何:

args="$*" && printf '%s\n' "${args##* }"

为了更安全地扩展"$*",请设置IFS(每个@StéphaneChazelas):

( IFS=' ' args="$*" && printf '%s\n' "${args##* }" )

但是,如果您的位置参数可以包含空格,则上述操作将失败。在这种情况下,请改用以下方法:

for a in "$@"; do : ; done && printf '%s\n' "$a"

请注意,这些技术避免使用eval且没有副作用。

shellcheck.net测试


1
如果最后一个参数包含空格,则第一个失败。
cuonglm '16

1
请注意,第一个也不适用于POSIX之前的版本
cuonglm '16

@cuonglm很好发现,您的正确观察现已合并。
AsymLabs

它还假定的第一个字符$IFS是空格。
斯特凡Chazelas

1
如果环境中没有$ IFS,则没有指定,否则指定。但是,由于实际上每次使用split + glob运算符时都需要设置IFS(不使用扩展名,所以无需处理),处理IFS的一种方法是在需要时进行设置。IFS=' '仅在这里明确表明它已用于的扩展就不会有什么害处"$*"
斯特凡Chazelas

3

尽管这个问题才2年多了,但我认为我会选择一个更为紧凑的选择。

print_last_arg () {
    echo "${@:${#@}:${#@}}"
}

让我们运行它

print_last_arg foo bar baz
baz

Bash shell参数扩展

编辑

更简洁: echo "${@: -1}"

(注意空间)

资源

已在macOS 10.12.6上进行测试,但也应返回大多数可用* nix风格的最后一个参数...

伤害少得多 ¯\_(ツ)_/¯


这应该是公认的答案。更好的是:echo "${*: -1}"shellcheck不会抱怨。
汤姆·黑尔

4
${array:n:m:}是扩展,无法与普通的POSIX sh一起使用。(该问题确实明确提及/bin/sh
ilkkachu

2

POSIXly:

while [ "$#" -gt 1 ]; do
  shift
done

printf '%s\n' "$1"

(此方法也适用于旧的Bourne外壳)

使用其他标准工具:

awk 'BEGIN{print ARGV[ARGC-1]}' "$@"

(这不适用于old awk,而old 则没有ARGV


仍然有伯恩贝壳的地方,awk也可能是没有的旧awk ARGV
斯特凡Chazelas

@StéphaneChazelas:同意。至少它与Brian Kernighan自己的版本一起使用。你有理想吗?
cuonglm '16

这消除了参数。如何保存它们?

@BinaryZebra:使用该awk方法,或for像其他方法一样使用其他普通循环,或将它们传递给函数。
cuonglm '16

2

这应该可以与任何POSIX兼容的外壳一起使用,并且也可以与POSIX之前的旧版Solaris Bourne外壳一起使用:

do=;for do do :;done;printf "%s\n" "$do"

这是基于相同方法的函数:

print_last_arg()
  if [ "$*" ]; then
    for do do :;done
    printf "%s\n" "$do"
  else
    echo
  fi

PS:不要告诉我我忘记了功能主体周围的花括号;-)


2
您忘记了函数主体;-)周围的花括号。

@BinaryZebra我警告过您;-)我没有忘记他们。括号在这里令人惊讶地是可选的。
jlliagre '16

1
@jlliagre我确实被警告了;-):P ......而且:当然!

语法规范的哪一部分允许这样做?
汤姆·黑尔

@TomHale Shell语法规则允许循环中缺少任何内容in ...for并且在if用作函数体的语句周围没有花括号。参见for_clausecompound_commandpubs.opengroup.org/onlinepubs/9699919799中
jlliagre

2

来自“ Unix-常见问题”

(1)

unset last
if    [ $# -gt 0 ]
then  eval last=\${$#}
fi
echo  "$last"

如果参数个数可以为零,则将参数零$0(通常是脚本的名称)分配给$last。这就是为什么的原因。

(2)

unset last
for   last
do    :
done
echo  "$last"

(3)

for     i
do
        third_last=$second_last
        second_last=$last
        last=$i
done
echo    "$last"

为了避免在没有参数的情况下打印空行,请替换echo "$last"为:

${last+false} || echo "${last}"

避免零参数计数if [ $# -gt 0 ]

这不是页面链接中内容的确切副本,但进行了一些改进。


0

这应该在所有perl已安装的系统上起作用(因此大多数UNICES):

print_last_arg () {
    printf '%s\0' "$@" | perl -0ne 's/\0//;$l=$_;}{print "$l\n"'
}

诀窍是用来在每个shell参数之后printf添加一个\0,然后perl在的-0开关中将其记录分隔符设置为NULL。然后,我们遍历输入,删除\0和,将每一个以NULL结尾的行保存为$l。读取所有输入后,将执行该END块(即为块}{),因此将打印最后一个“行”:最后一个shell参数。


@mikeserv啊,是的,除了最后一个参数,我没有在换行符中进行过测试。编辑后的版本几乎可以处理任何事情,但是对于这样一个简单的任务来说可能太复杂了。
terdon

是的,对我来说,调用一个外部可执行文件(如果它还不是逻辑的一部分)有点费力。但如果一点是要传递给这样的说法sedawk或者perl那么它可能是反正有价值的信息。仍然可以sed -z对最新的GNU版本执行相同的操作。
mikeserv '16

你为什么被拒绝?很好...我想吗?
mikeserv '16

0

这是使用递归的版本。不知道这如何与POSIX兼容...

print_last_arg()
{
    if [ $# -gt 1 ] ; then
        shift
        echo $( print_last_arg "$@" )
    else
        echo "$1"
    fi
}

0

一个简单的概念,没有算术,没有循环,没有eval,只是功能。
请记住,Bourne shell没有算术(需要external expr)。如果要获得免费的算术eval自由选择,则可以选择。需要功能意味着SVR3或更高版本(不覆盖参数)。
在下面查找带有printf的更强大的版本。

printlast(){
    shift "$1"
    echo "$1"
}

printlast "$#" "$@"          ### you may use ${1+"$@"} here to allow
                             ### a Bourne empty list of arguments,
                             ### but wait for a correct solution below.

这种结构的呼叫printlast固定的,参数需要在外壳参数列表进行设置$1$2等等(参数堆栈),并做了呼叫给出。

如果参数列表需要更改,只需设置它们即可:

set -- o1e t2o t3r f4u
printlast "$#" "$@"

或创建一个易于使用的函数(getlast),该函数可以允许通用参数(但速度不快,参数被两次传递)。

getlast(){ printlast "$#" "$@"; }
getlast o1e t2o t3r f4u

请注意,参数(of getlast或在$@printlast中包含的所有参数)可以带有空格,换行符等。但不能为NUL。

更好

0当参数列表为空时,此版本不打印,并使用更强大的printf(echo如果external printf对于旧的shell不可用,则回退)。

printlast(){ shift  "$1"; printf '%s' "$1"; }
getlast  ()  if     [ $# -gt 0 ]
             then   printlast "$#" "$@"
                    echo    ### optional if a trailing newline is wanted.
             fi
### The {} braces were removed on purpose.

getlast 1 2 3 4    # will print 4

使用EVAL。

如果Bourne shell甚至更旧并且没有功能,或者由于某种原因使用eval是唯一的选择:

要打印值:

if    [ $# -gt 0 ]
then  eval printf "'1%s\n'" \"\$\{$#\}\"
fi

要在变量中设置值:

if    [ $# -gt 0 ]
then  eval 'last_arg='\"\$\{$#\}\"
fi

如果需要在函数中完成此操作,则需要将参数复制到函数中:

print_last_arg () {
    local last_arg                ### local only works on more modern shells.
    eval 'last_arg='\"\$\{$#\}\"
    echo "last_arg3=$last_arg"
}

print_last_arg "$@"               ### Shell arguments are sent to the function.

更好,但是它说不使用循环-而是使用循环。数组副本是一个循环。具有两个功能,它使用两个循环。还-是否有某些原因要遍历整个数组而不是对其进行索引eval
mikeserv '16

1
你看到的任何for loopwhile loop


1
按照您的标准,我猜所有代码都有循环,甚至一个echo "Hello"循环也有循环,因为CPU必须在循环中读取内存位置。再说一遍:我的代码没有添加循环。

1
我真的不在乎您对好坏的看法,已经多次证明它是错误的。还是那句话:我的代码有没有forwhileuntil循环。您在代码中看到任何for whileuntil循环吗?不?然后接受事实,拥抱事实并学习。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.