符合POSIX标准的shell中的数组

26
根据hyperpolyglot.org上的参考表,以下语法可用于设置数组。
i=(1 2 3)

但我在Ubuntu上使用默认的/bin/sh,也就是dash时出现了错误,它应该符合POSIX标准。

# Trying the syntax with dash in my terminal
> dash -i
$ i=(1 2 3)
dash: 1: Syntax error: "(" unexpected
$ exit

# Working fine with bash
> bash -i
$ i=(1 2 3)
$ echo ${i[@]}
1 2 3
$ exit

参考文档是否误导或错误?
如果是,定义数组或列表的正确方式应该如何,并符合POSIX标准?

7
POSIX 中没有数组。如果你仔细看,那是针对 字面量 的。hyperpolyglot.org 上的整个章节都是完全错误的(可能是由 M$ 完成的)。 - David C. Rankin
我不理解这个表格在这个上下文中所谓的“字面量”是什么意思,这个表格的部分被称为“可调整大小的数组”。但即使它不是关于数组的,根据参考表格,它也应该正确执行。但你说得对,重要的是没有数组的概念。 - zoom
谢谢您的澄清,我本来考虑依赖这张表格,现在我会寻找另一份。 - zoom
3
哎呀,这个“参考”太糟糕了。它使用了非POSIX的function关键字,神奇的$RANDOMecho -n命令,建议使用trap exit ERR而不是更有用的trap 'exit 1' ERR,并且在引用上极其鲁莽。不建议使用。 - Toby Speight
4个回答

30

Posix没有指定数组,因此如果您受限于Posix shell功能,则无法使用数组。

恐怕您的参考资料是错误的。遗憾的是,您在互联网上发现的并不是全部正确的信息。


我也看到了那个,但我需要确认一下。那个表格看起来非常详细和精确,让我感到困惑。 - zoom
2
@zoom 佛罗里达州沼泽地的供应商也提供详细和精确的勘测报告。 - rici
1
是的,但一个常见的困惑是当像 bash 这样的 shell 以 POSIX 模式运行时,例如使用 --posix 调用或 #!/bin/sh shebang,那么它将默默地理解这个非 POSIXism。 - Jack Wasey
这只是部分正确的。POSIX确实指定了可以用作$@的参数列表,可以通过shiftset进行调整,如我的回答中所述。 - Adam Katz

26

正如rici所说,Dash不支持数组。然而,如果您要做的是编写循环,那么还是有解决方法的。

for循环不能处理数组,但是您可以使用while循环和read内置函数来完成分隔。由于Dash的read内置函数也不支持分隔符,因此您需要解决这个问题。

以下是一个示例脚本:

myArray="a b c d"

echo "$myArray" | tr ' ' '\n' | while read item; do
  # use '$item'
  echo $item
done

关于这个问题的更深入解释:

  • tr ' ' '\n'会让您进行单字符替换,其中删除空格并添加换行符 - 这是 read内置命令的默认分隔符。

  • 当检测到标准输入已关闭时(也就是当输入已完全处理时),read将使用失败的退出代码退出。

  • 由于echo在其输入之后会多打印一个换行符,这将使您可以处理数组中的最后一个“元素”。

这相当于bash代码:

myArray=(a b c d)

for item in ${myArray[@]}; do
  echo $item
done
如果您想检索第n个元素(假设为第2个,以此为例):
myArray="a b c d"

echo $myArray | cut -d\  -f2 # change -f2 to -fn

1
谢谢!最后一个代码片段正是我在寻找的 :) - kaiya
2
问题在于它试图将几个独立的字符串存储在一个单一的字符串中。一旦你想要存储包含空格字符的字符串,你就无法正确地检索它们。你必须使用一些其他安全的分隔符而不是空格,并实现自己的字符串解析。 - Kusalananda
如果数组包含文件路径,则文件名中可以使用空格,但也可以使用其他符号,例如管道“|”。因此,您可以尝试将“myArray =” a b c d“更改为”myArray = a | b | c | d“,然后将”tr' ''\ n '“更改为”tr '|' '\n'“。我已经进行了测试,它可以正常工作。 - Sergey Ponomarev
shellcheck似乎建议使用while read -r item; do以防止任何反斜杠字符的混淆。 - Abhishek Chakravarti
为什么要使用外部命令 tr 呢?你已经失去了空格,为什么不直接使用 for item in $myArray(注意没有引号)呢?如果你想保留空格,可以将其放入函数中,并在本地更改 $IFS 为所需的分隔符(例如 local IFS='|')... 或者使用 $@(请参见我的答案)。 - Adam Katz

19
确实,POSIX sh shell 没有像 bash 和其他 shell 那样的命名数组,但是有一个列表,sh shell (以及 bash 和其他 shell)可以使用,那就是“位置参数”列表。
该列表通常包含传递给当前脚本或 shell 函数的参数,但您可以使用 set 内置命令来设置其值。
#!/bin/sh

set -- this is "a list" of "several strings"

在上述脚本中,位置参数$1$2等被设置为五个字符串。使用--是为了确保您不会意外地设置一个shell选项(set命令也能够这样做)。只有当第一个参数以-开头时才会出现这种情况。
例如,要循环遍历这些字符串,可以使用:
for string in "$@"; do
    printf 'Got the string "%s"\n' "$string"
done

或者更短

for string do
    printf 'Got the string "%s"\n' "$string"
done

或者只需
printf 'Got the string "%s"\n' "$@"

set 对于将通配符扩展为路径名列表也非常有用:

#!/bin/sh

set -- "$HOME"/*/

# "visible directory" below really means "visible directory, or visible 
# symbolic link to a directory".

if [ ! -d "$1" ]; then
    echo 'You do not have any visible directories in your home directory'
else
    printf 'There are %d visible directories in your home directory\n' "$#"

    echo 'These are:'
    printf '\t%s\n' "$@"
fi
shift内置命令可用于从列表中移除第一个位置参数。
#!/bin/sh

# pathnames
set -- path/name/1 path/name/2 some/other/pathname

# insert "--exclude=" in front of each
for pathname do
    shift
    set -- "$@" --exclude="$pathname"
done

# call some command with our list of command line options
some_command "$@"


在POSIX中是否有类似于unshift的东西?你能否以某种方式使用$@作为堆栈? - Lassi
7
在开头添加:set -- "$item" "$@"。在结尾添加:set -- "$@" "$item"。你不能轻松删除 $@ 的最后一个元素,但是你可以使用 shift 命令来删除第一个元素。可以通过将元素插入/弹出数组的开头来实现堆栈操作。 - Kusalananda
删除 $@ 的最后一个元素只是稍微有些麻烦。我添加了一个答案,其中演示了 shiftunshiftpushpop(执行该删除操作)以及其他用于 $@ 的数组函数。 - Adam Katz

3

在 POSIX shell 中,您可以使用参数列表 $@ 作为数组

初始化、shiftunshiftpush 非常简单:

# initialize $@ containing a string, a variable's value, and a glob's matches
set -- "item 1" "$variable" *.wav

# shift (remove first item, accepts a numeric argument to remove more)
shift

# unshift (prepend new first item)
set -- "new item" "$@"

# push (append new last item)
set -- "$@" "new item"

这是一个 pop 实现示例:
# pop (remove last item, store it in $last)
i=0
for last in "$@"; do 
  if [ $((i+=1)) = 1 ]; then set --; fi  # increment $i. first run: empty $@
  if [ $i = $# ]; then break; fi         # stop before processing the last item
  set -- "$@" "$last"                    # add $a back to $@
done
echo "$last has been removed from ($*)"

($*$@ 中的内容使用 $IFS 进行连接,默认情况下为一个空格字符。)

遍历 $@ 数组并修改其中一些内容:

i=0
for a in "$@"; do 
  if [ $((i+=1)) = 1 ]; then set --; fi  # increment $i. first run: empty $@
  a="${a%.*}.mp3"       # example tweak to $a: change extension to .mp3
  set -- "$@" "$a"      # add $a back to $@
done

参考 $@ 数组中的项目:

echo "$1 is the first item"
echo "$# is the length of the array"
echo "all items in the array (properly quoted): $@"
echo "all items in the array (in a string): $*"
[ "$n" -ge 0 ] && eval "echo \"the ${n}th item in the array is \$$n\""

(eval很危险,所以在运行之前我确保$n是一个数字)

有几种方法可以将$last设置为列表的最后一个项目而不弹出它:
使用函数:

last_item() { shift $(($# - 1)) 2>/dev/null && printf %s "$1"; }
last="$(last_item "$@")"

...或者使用eval(因为$#始终是一个数字,所以是安全的):

eval last="\$$#"

...或者使用循环:

for last in "$@"; do true; done

⚠️ 警告: 函数有它们自己的$@数组。如果是只读的,你必须将其传递给函数,例如my_function "$@",否则如果你想操作$@并且不希望在项值中出现空格,则使用set -- $(my_function "$@")

如果需要处理项值中的空格,则变得更加麻烦:

# ensure my_function() returns each list item on its own line
i=1
my_function "$@" |while IFS= read line; do
  if [ $i = 1 ]; then unset i; set --; fi
  set -- "$@" "$line"
done

这仍然无法处理条目中的换行符。你必须将它们转义成另一个字符(但不是空字符),然后稍后再将它们转义回来。请参见上面的“遍历$@数组并修改其中一些内容”。你可以在for循环中遍历数组,然后运行函数,在while IFS= read line循环中修改变量,或者在for循环中执行所有操作而不使用函数。


for i in "$@"; do ...; done 可以简写为 for i do ...; done。这样可以省去用户记得引用 $@ 的麻烦。 - Kusalananda
@Kusalananda - 你添加到我的答案中的大部分引用都是不必要的(数字永远不需要引号;如果您更改$IFS以包括数字,那么您将不会喜欢后果)。您从一个需要引号的实例中删除了引号。我已还原了您进行的大部分更改。是的,'for i do …; done'更短且符合POSIX标准,但我不认为它很直观。 - Adam Katz
由于Shell不会在赋值的右侧执行拆分或通配符扩展,因此不需要将引号用于该位置。如果您仅仅因为包含数字就使用扩展时去除了引号,那么最好说明您假设IFS永远不会包含数字,或者明确重置IFS为其默认值。不引用这些扩展所获得的好处非常微小。 - Kusalananda

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接