Lập chỉ mục và sửa đổi mảng tham số Bash $ @


11

Có thể tham khảo các chỉ số trong $@? Tôi không thể tìm thấy bất kỳ tài liệu tham khảo nào để sử dụng như bất kỳ nơi nào sau đây trong wiki của GrayCatHướng dẫn về kịch bản nâng caonhững người khác chỉ định điều này cho một biến khác trước khi sửa đổi điều đó.

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

Mục tiêu là DRY : Đối số đầu tiên được sử dụng cho một thứ và phần còn lại cho một thứ khác và tôi muốn tránh sao chép mã để chuẩn hóa, $@mảng hoặc để tạo một hàm riêng cho điều này (mặc dù tại đây điểm có lẽ là cách dễ nhất).

Làm rõ: Đối tượng là sửa đổi các giá trị của độ dài biến $@ để làm cho dễ gỡ lỗi hơn. Phiên bản hiện tại hơi quá hack theo ý thích của tôi, mặc dù nó hoạt động ngay cả đối với những con đường kỳ quái như

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

Cập nhật : Có vẻ như điều này là không thể. hiện sử dụng cả mã và sao chép dữ liệu, nhưng ít nhất nó hoạt động:

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"
}

Bounty tìm đến bất kỳ ai có thể thoát khỏi sự trùng lặp mã để thu gọn các dấu gạch chéo hoặc sao chép dữ liệu để giữ $1và các tham số khác, hoặc cả hai, trong khi giữ mã ở kích thước hợp lý và thành công trong tất cả các thử nghiệm đơn vị:

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

Câu trả lời:


16

POSIX

Để bình thường hóa các dấu gạch chéo trong tất cả các tham số, tôi sẽ sử dụng thủ thuật xoay đối số: $1tắt, chuyển đổi và đặt kết quả ở cuối danh sách tham số. Nếu bạn làm điều đó nhiều lần như có các tham số, bạn đã chuyển đổi tất cả các tham số và bạn đã đưa chúng trở lại theo thứ tự.

Đối với phần thứ hai của mã, tôi đã thay đổi logic của bạn để ít gây nhầm lẫn hơn: vòng lặp bên ngoài lặp lại các tham số và vòng lặp bên trong lặp lại trên các thành phần đường dẫn. for x; do … doneLặp lại các tham số vị trí, đó là một thành ngữ thuận tiện. Tôi sử dụng một cách phù hợp với POSIX để khớp một chuỗi với một mẫu: casecấu trúc.

Đã thử nghiệm với dấu gạch ngang 0,5.5.1, pdksh 5.2,14, bash 3.2,39, bash 4.1.5, ksh 93s +, zsh 4.3.10.

Lưu ý bên lề: dường như có một lỗi trong bash 4.1.5 (không phải trong 3.2): nếu mẫu trường hợp là "${common_path%/}"/*, một trong các thử nghiệm thất bại.

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

Nếu bạn đang ở trong bash (hoặc ksh), bạn có thể sử dụng mảng - Tôi không hiểu tại sao bạn dường như tự giới hạn mình trong các tham số vị trí. Đây là một phiên bản sử dụng một mảng. Tôi phải thừa nhận rằng nó không đặc biệt rõ ràng hơn phiên bản POSIX, nhưng nó tránh được sự xáo trộn ban đầu.

Đối với phần dấu gạch chéo bình thường, tôi sử dụng ksh93 xây dựng ${foo//PATTERN/REPLACEMENT}xây dựng để thay thế tất cả các lần xuất hiện của PATTERNtrong $foobằng REPLACEMENT. Mẫu này +(\/)phù hợp với một hoặc nhiều dấu gạch chéo; dưới bash, shopt -s extglobphải có hiệu lực (tương đương, bắt đầu bash với bash -O extglob). Cấu trúc set ${!a[@]}đặt các tham số vị trí vào danh sách các chỉ số của mảng a. Điều này cung cấp một cách thuận tiện để lặp qua các phần tử của mảng.

Đối với phần thứ hai, tôi cùng logic vòng lặp với phiên bản POSIX. Lần này, tôi có thể sử dụng [[ … ]]vì tất cả các shell được nhắm mục tiêu ở đây đều hỗ trợ nó.

Đã thử nghiệm với 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"
}

zsh

Đáng buồn thay, zsh thiếu ${!array[@]}tính năng để thực thi phiên bản ksh93 như hiện tại. May mắn thay, zsh có hai tính năng làm cho phần đầu tiên trở nên dễ dàng. Bạn có thể lập chỉ mục các tham số vị trí như thể chúng là @mảng, vì vậy không cần sử dụng mảng trung gian. Và zsh có lặp cấu trúc mảng : "${(@)array//PATTERN/REPLACEMENT}"thực hiện việc thay thế mẫu trên mỗi phần tử mảng lần lượt, và đánh giá lại đến các mảng kết quả (mức gây nhầm lẫn, bạn không cần dấu ngoặc kép mặc dù kết quả là nhiều từ, đây là một sự tổng quát của "$@"). Phần thứ hai về cơ bản là không thay đổi.

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"
}

Các trường hợp thử nghiệm

Các giải pháp của tôi được kiểm tra và bình luận tối thiểu. Tôi đã thay đổi cú pháp của các trường hợp thử nghiệm của bạn để phân tích cú pháp trong các shell không có $'…'và báo cáo các lỗi theo cách thuận tiện hơn.

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, thật tuyệt vời. Nhiều hơn tôi yêu cầu, ở mức nào. Bạn, thưa ngài, là tuyệt vời.
l0b0

Trong cuộc thảo luận POSIX, trong vòng lặp đầu tiên nơi bạn bình thường hóa dấu gạch chéo, tại sao lại thêm "." với sprintf sau đó tước nó trong dòng tiếp theo? Mã dường như hoạt động mà không có nó, nhưng tôi nghi ngờ bạn đang xử lý một trường hợp cạnh mà tôi không biết.
Alan De Smet

1
@AlanDeSmet Trường hợp cạnh là nếu chuỗi kết thúc bằng một dòng mới. Lệnh thay thế dải ra khỏi dòng mới.
Gilles 'SO- ngừng trở nên xấu xa

6

Tại sao bạn không sử dụng $ 1, $ 2 .. $ 9, $ {10}, $ {11} .. vân vân? Nó thậm chí còn KHÔ hơn - những gì bạn đang cố gắng làm :)

Thông tin thêm về mối quan hệ giữa $ number và $ @:

$ @ có thể được coi là tốc ký cho "tất cả các phần tử của một mảng chứa tất cả các đối số"

Vì vậy, $ @ là một loại tốc ký của $ {args [@]} (tranh luận ở đây là một mảng 'ảo' chứa tất cả các đối số - không phải là một biến thực, làm phiền bạn)

$ 1 là $ {args [1]}, $ 2 là $ {args [2]}, v.v.

Khi bạn nhấn [9], hãy sử dụng dấu ngoặc nhọn: $ {10} là $ {args [10]}, $ {11} là $ {args [11]}, v.v.


Sử dụng gián tiếp một đối số dòng lệnh

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

Thí dụ:

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

Nhược điểm rõ ràng của việc phải sử dụng $ * number * là bạn không thể sử dụng biến chỉ số như với ${args[$i]}.
trực giác

@intuited sau đó sử dụng gián tiếp; Tôi sẽ chỉnh sửa câu trả lời của tôi.
pepoluan

5

Đối số đầu tiên được sử dụng cho một thứ và phần còn lại cho một thứ khác,

Tôi nghĩ những gì bạn muốn là 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

Tôi hoàn toàn không biết tại sao bạn không chỉ sử dụng $ 1 $ 2, v.v. nhưng .. Điều này có thể phù hợp với nhu cầu của bạn.

$ 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"

đầu ra

  -------- 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 hoạt động của bất cứ điều gì theo sau nó, để tạo ra $ 1, $ 2 .. vv .. Điều này tất nhiên sẽ ghi đè lên các giá trị ban đầu, vì vậy chỉ cần lưu ý về điều đó.


ahh ... vậy bởi 'eval' bạn có nghĩa là tham chiếu gián tiếp ... $ {! var} xây dựng an toàn hơn, giống như những gì tôi đã viết trong câu trả lời của mình
pepoluan

@pepoluan ... Cảm ơn bạn đã cảnh báo tôi về điều đó. Đó là đơn giản hơn nhiều để ghi ... (Tôi đã trở lại chỉ là bây giờ ra đi đến trang web tôi gọi, Nếu tôi đã đọc thêm, tôi đã thấy nó đề cập đến nó có quá :( ....
Peter.O

heh nhưng nếu gián tiếp xảy ra trên trái bên, eval là một điều ác cần thiết, tho' :)
pepoluan

@peopluan ... được rồi, cảm ơn vì đã chỉ ra điều đó ... và chỉ là một bên: Tôi không hiểu tại sao evalmột số người lại coi là evil... (có lẽ là do chính tả :) ... Nếu evallà "xấu", thì $ {! Var} có "xấu" như nhau không? ... Đối với tôi nó chỉ là một phần của ngôn ngữ và là một phần hữu ích, lúc đó .. nhưng tôi chắc chắn thích $ {! Var} ...
Peter.O

1

Lưu ý tôi hỗ trợ không gian trong tên tệp.

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}}"
}

Tôi đã thêm một trường hợp thử nghiệm cho tên tệp có khoảng trắng và cố định 2 thử nghiệm bị thiếu hàng đầu /

    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
}
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.