索引和修改Bash参数数组$ @


11

是否可以引用中的索引$@?我在GrayCat的wiki中的任何地方都找不到像下面这样的引用,而Advanced Scripting Guide其他脚本在修改它之前将其分配给其他变量。

$ echo ${@[0]}
-bash: ${@[0]}: bad substitution

目标是DRY:第一个参数用于一件事情,其余参数用于其他事情,我想避免重复代码以标准化,$@数组或为此创建单独的函数(尽管在此情况下)指出这可能是最简单的出路)。

澄清:目的是修改可变长度 的值,$@以使代码更易于调试。尽管我什至喜欢像这样的怪异路径,但当前的版本还是有点过于hacky

$'--$`\! *@ \a\b\e\E\f\r\t\v\\\"\' \n'

更新:看来这是不可能的。该代码现在同时使用代码和数据重复,但是至少可以使用:

path_common()
{
    # Get the deepest common path.
    local common_path="$(echo -n "${1:-}x" | tr -s '/')"
    common_path="${common_path%x}"
    shift # $1 is obviously part of $1
    local path

    while [ -n "${1+defined}" ]
    do
        path="$(echo -n "${1}x" | tr -s '/')"
        path="${path%x}"
        if [[ "${path%/}/" = "${common_path%/}/"* ]]
        then
            shift
        else
            new_common_path="${common_path%/*}"
            [ "$new_common_path" = "$common_path" ] && return 1 # Dead end
            common_path="$new_common_path"
        fi
    done
    printf %s "$common_path"
}

任何人都可以摆脱代码重复以折叠重复的斜杠或保留的数据重复$1和其他参数,或同时保留这两个参数,同时保持代码的合理大小并成功进行所有单元测试:

test "$(path_common /a/b/c/d /a/b/e/f; echo x)" = /a/bx
test "$(path_common /long/names/foo /long/names/bar; echo x)" = /long/namesx
test "$(path_common / /a/b/c; echo x)" = /x
test "$(path_common a/b/c/d a/b/e/f ; echo x)" = a/bx
test "$(path_common ./a/b/c/d ./a/b/e/f; echo x)" = ./a/bx
test "$(path_common $'\n/\n/\n' $'\n/\n'; echo x)" = $'\n/\n'x
test "$(path_common --/-- --; echo x)" = '--x'
test "$(path_common '' ''; echo x)" = x
test "$(path_common /foo/bar ''; echo x)" = x
test "$(path_common /foo /fo; echo x)" = x
test "$(path_common $'--$`\! *@ \a\b\e\E\f\r\t\v\\\"\' \n' $'--$`\! *@ \a\b\e\E\f\r\t\v\\\"\' \n'; echo x)" = $'--$`\! *@ \a\b\e\E\f\r\t\v\\\"\' \n'x
test "$(path_common /foo/bar //foo//bar//baz; echo x)" = /foo/barx
test "$(path_common foo foo; echo x)" = foox
test "$(path_common /fo /foo; echo x)" = x

Answers:


16

POSIX

为了规范化所有参数中的斜杠,我将使用旋转参数技巧:$1移开,转换它并将结果放在参数列表的末尾。如果您执行的次数与有参数一样多,则说明您已转换了所有参数,并已按顺序重新设置它们。

对于代码的第二部分,我更改了逻辑以减少混乱:外循环遍历参数,而内循环遍历路径组件。for x; do … done迭代位置参数,这是一个方便的习惯用法。我使用POSIX兼容的方式将字符串与模式进行匹配:case构造。

使用破折号0.5.5.1,pdksh 5.2.14,bash 3.2.39,bash 4.1.5,ksh 93s +,zsh 4.3.10进行了测试。

旁注:bash 4.1.5中似乎存在一个错误(不是3.2中的错误):如果case模式为"${common_path%/}"/*,则测试之一将失败。

posix_path_common () {
  for tmp; do
    tmp=$(printf %s. "$1" | tr -s "/")
    set -- "$@" "${tmp%.}"
    shift
  done
  common_path=$1; shift
  for tmp; do
    while case ${tmp%/}/ in "${common_path%/}/"*) false;; esac; do
      new_common_path=${common_path%/*}
      if [ "$new_common_path" = "$common_path" ]; then return 1; fi
      common_path=$new_common_path
    done
  done
  printf %s "$common_path"
}

bash,ksh

如果您使用bash(或ksh),则可以使用数组-我不明白为什么您似乎将自己限制在位置参数上。这是使用数组的版本。我必须承认它并不比POSIX版本特别清晰,但是它确实避免了最初的n ^ 2改组。

对于斜线归一化部分,我使用ksh93构造${foo//PATTERN/REPLACEMENT}构造将所有PATTERNin 出现替换$fooREPLACEMENT。模式是+(\/)匹配一个或多个斜线;在bash下,shopt -s extglob必须生效(等效于,以bash开始bash -O extglob)。该结构set ${!a[@]}将位置参数设置为数组的下标列表a。这提供了一种方便的方法来迭代数组的元素。

对于第二部分,我使用与POSIX版本相同的循环逻辑。这次,我可以使用,[[ … ]]因为这里的所有目标外壳都支持它。

使用bash 3.2.39,bash 4.1.5,ksh 93s +测试。

array_path_common () {
  typeset a i tmp common_path new_common_path
  a=("$@")
  set ${!a[@]}
  for i; do
    a[$i]=${a[$i]//+(\/)//}
  done
  common_path=${a[$1]}; shift
  for tmp; do
    tmp=${a[$tmp]}
    while [[ "${tmp%/}/" != "${common_path%/}/"* ]]; do
      new_common_path="${common_path%/*}"
      if [[ $new_common_path = $common_path ]]; then return 1; fi
      common_path="$new_common_path"
    done
  done
  printf %s "$common_path"
}

sh

遗憾的是,zsh缺少按${!array[@]}原样执行ksh93版本的功能。幸运的是,zsh具有两个功能,使第一部分变得轻而易举。您可以索引位置参数,就像它们是@数组一样,因此无需使用中间数组。zsh具有数组迭代构造"${(@)array//PATTERN/REPLACEMENT}"依次对每个数组元素执行模式替换,并计算结果数组(令人困惑的是,即使结果是多个单词,您也需要双引号;这是的概括"$@")。第二部分基本不变。

zsh_path_common () {
  setopt local_options extended_glob
  local tmp common_path new_common_path
  set -- "${(@)@//\/##//}"
  common_path=$1; shift
  for tmp; do
    while [[ "${tmp%/}/" != "${common_path%/}/"* ]]; do
      new_common_path="${common_path%/*}"
      if [[ $new_common_path = $common_path ]]; then return 1; fi
      common_path="$new_common_path"
    done
  done
  printf %s "$common_path"
}

测试用例

我的解决方案经过最少的测试和评论。我已经更改了测试用例的语法,以便在没有外壳的情况下进行解析,并$'…'以更方便的方式报告失败。

do_test () {
  if test "$@"; then echo 0; else echo $? "$@"; failed=$(($failed+1)); fi
}

run_tests () {
  function_to_test=$1; shift
  failed=0
  do_test "$($function_to_test /a/b/c/d /a/b/e/f; echo x)" = /a/bx
  do_test "$($function_to_test /long/names/foo /long/names/bar; echo x)" = /long/namesx
  do_test "$($function_to_test / /a/b/c; echo x)" = /x
  do_test "$($function_to_test a/b/c/d a/b/e/f ; echo x)" = a/bx
  do_test "$($function_to_test ./a/b/c/d ./a/b/e/f; echo x)" = ./a/bx
  do_test "$($function_to_test '
/
/
' '
/
'; echo x)" = '
/
'x
  do_test "$($function_to_test --/-- --; echo x)" = '--x'
  do_test "$($function_to_test '' ''; echo x)" = x
  do_test "$($function_to_test /foo/bar ''; echo x)" = x
  do_test "$($function_to_test /foo /fo; echo x)" = x
  do_test "$($function_to_test '--$`\! *@ \a\b\e\E\f\r\t\v\\\"'\'' 
' '--$`\! *@ \a\b\e\E\f\r\t\v\\\"'\'' 
'; echo x)" = '--$`\! *@ \a\b\e\E\f\r\t\v\\\"'\'' 
'x
  do_test "$($function_to_test /foo/bar //foo//bar//baz; echo x)" = /foo/barx
  do_test "$($function_to_test foo foo; echo x)" = foox
  do_test "$($function_to_test /fo /foo; echo x)" = x
  if [ $failed -ne 0 ]; then echo $failed failures; return 1; fi
}

1
+50,哇。无论如何,都比我要的要多。先生,您真棒。
l0b0

在POSIX讨论中,在标准化斜杠的第一个循环中,为什​​么要附加“。” 与sprintf然后在下一行将其剥离?没有它,代码似乎可以工作,但是我怀疑您正在处理一个我不知道的边缘情况。
艾伦·德·史梅特

1
@AlanDeSmet边缘情况是字符串是否以换行符结尾。命令替换会删除尾随的换行符。
吉尔斯(Gillles)“所以-别再邪恶了”,

6

您为什么不只使用$ 1,$ 2 .. $ 9,$ {10},$ {11} ..等等呢?比您尝试做的还要 -)

有关$ 数字和$ @ 之间的关系的更多信息:

$ @可以视为“包含所有参数的数组的所有元素”的简写

因此,$ @是$ {args [@]}的简写形式(args是一个包含所有参数的“虚拟”数组,请注意,这不是一个实数变量)

$ 1是$ {args [1]},$ 2是$ {args [2]},依此类推。

当您按下[9]时,请使用大括号:$ {10}是$ {args [10]},$ {11}是$ {args [11]},依此类推。


间接使用命令行参数

argnum=3  # You want to get the 3rd arg
do-something ${!argnum}  # Do something with the 3rd arg

例:

argc=$#
for (( argn=1; argn<=argc; argn++)); do
    if [[ ${!argn} == "foo" ]]; then
        echo "Argument $argn of $argc is 'foo'"
    fi
done

不得不使用$ * number *的明显缺点是,您不能像那样使用索引变量${args[$i]}
直觉

@intuited然后使用间接;我将编辑答案。
pepoluan

5

第一个参数用于一件事情,其余的用于其他事情,

我想你想要的是 shift

$ set one two three four five
$ echo $@
one two three four five
$ echo $1
one
$ foo=$1
$ echo $foo
one
$ shift
$ echo $@
two three four five
$ shift 2
$ echo $@
four five
$ echo $1
four

1

我不太清楚为什么您不只使用$ 1 $ 2等。但是..这可能适合您的需求。

$ script "ed    it" "cat/dog"  33.2  \D  

  echo "-------- Either use 'indirect reference'"
  for ((i=1;i<=${#@};i++)) ;do
    #  eval echo \"\$$i\" ..works, but as *pepoluan* 
    #    has pointed out: echo "${!i}" ..is better.
    echo "${!i}"
  done
  echo "-------- OR use an array"
  array=("$@")
  for ((i=0;i<${#array[@]};i++)) ;do
    echo "${array[$i]}" 
  done
  echo "-------- OR use 'set'"
  set  "$@"
  echo "$1"
  echo "$2"
  echo "$3"
  echo "$4"

输出

  -------- Either use 'indirect reference'
  ed    it
  cat/dog
  33.2
  D
  -------- OR use an array
  ed    it
  cat/dog
  33.2
  D
  -------- OR use 'set'
  ed    it
  cat/dog
  33.2
  D

set 它后面的任何东西的工作,以创建$ 1,$ 2 ..等。当然,这将覆盖原始值,因此请注意。


嗯...所以'eval'的意思是间接引用... $ {!var}构造更安全,就像我在回答中所写的一样
pepoluan 2011年

@pepoluan ...感谢您提醒我这一点。编写起来要简单得多...((我现在回到我所引用的网页,如果我进一步阅读,我也会看到它也提到了它:( ....
Peter.O,

呵呵。但是如果间接发生在左侧,则eval是必不可少的
恶作剧

@peopluan ...好吧,感谢您指出这一点...顺便提一句:我不明白为什么eval有人认为evil...(也许是因为拼写:) ...如果eval是“坏”,那么$ {!var}是否同样是“坏”?...对我来说,它只是语言的一部分,而且是有用的一部分..但是我绝对喜欢$ {!var} ...
Peter.O 2011年

1

注意我支持文件名中的空格。

function SplitFilePath {
    IFS=$'/' eval "${1}"=\( \${2} \)
}
function JoinFilePath {
    IFS=$'/' eval echo -n \"\${*}\"
    [ $# -eq 1 -a "${1}" = "" ] && echo -n "/"
}
function path_common {
    set -- "${@//\/\///}"       ## Replace all '//' with '/'
    local -a Path1
    local -i Cnt=0
    SplitFilePath Path1 "${1}"
    IFS=$'/' eval set -- \${2} 
    for CName in "${Path1[@]}" ; do
        [ "${CName}" != "${1}" ] && break;
        shift && (( Cnt++ ))
    done
    JoinFilePath "${Path1[@]:0:${Cnt}}"
}

我为带有空格的文件名添加了一个测试用例,并修复了缺少前导/的2个测试

    do_test () {

  if test "${@}"; then echo 0; else echo $? "$@"; failed=$(($failed+1)); fi
}

run_tests () {
  function_to_test=$1; shift
  failed=0
  do_test "$($function_to_test /a/b/c/d /a/b/e/f; echo x)" = /a/bx
  do_test "$($function_to_test /long/names/foo /long/names/bar; echo x)" = /long/namesx
  do_test "$($function_to_test / /a/b/c; echo x)" = /x      
  do_test "$($function_to_test a/b/c/d a/b/e/f ; echo x)" = a/bx
  do_test "$($function_to_test ./a/b/c/d ./a/b/e/f; echo x)" = ./a/bx
  do_test "$($function_to_test '
/
/
' '
/
'; echo x)" = '
/
'x
  do_test "$($function_to_test --/-- --; echo x)" = '--x'
  do_test "$($function_to_test '' ''; echo x)" = x
  do_test "$($function_to_test /foo/bar ''; echo x)" = x
  do_test "$($function_to_test /foo /fo; echo x)" = /x      ## Changed from x
  do_test "$($function_to_test '--$`\! *@ \a\b\e\E\f\r\t\v\\\"'\'' 
' '--$`\! *@ \a\b\e\E\f\r\t\v\\\"'\'' 
'; echo x)" = '--$`\! *@ \a\b\e\E\f\r\t\v\\\"'\'' 
'x
  do_test "$($function_to_test /foo/bar //foo//bar//baz; echo x)" = /foo/barx
  do_test "$($function_to_test foo foo; echo x)" = foox
  do_test "$($function_to_test /fo /foo; echo x)" = /x          ## Changed from x
  do_test "$($function_to_test "/fo d/fo" "/fo d/foo"; echo x)" = "/fo dx"

  if [ $failed -ne 0 ]; then echo $failed failures; return 1; fi
}
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.