การทำดัชนีและการปรับเปลี่ยนอาร์เรย์พารามิเตอร์ Bash $ @


11

เป็นไปได้$@หรือไม่ที่จะอ้างถึงดัชนีใน ฉันไม่พบการอ้างอิงใด ๆ ที่จะใช้สิ่งต่อไปนี้ในวิกิของ GrayCatและคู่มือการใช้สคริปต์ขั้นสูงและอื่น ๆกำหนดค่านี้ให้กับตัวแปรที่แตกต่างกันก่อนที่จะทำการแก้ไขแทน

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

เป้าหมายคือDRY : อาร์กิวเมนต์แรกใช้สำหรับสิ่งหนึ่งและส่วนที่เหลือเป็นอย่างอื่นและฉันต้องการหลีกเลี่ยงการทำซ้ำรหัสเพื่อทำให้เป็นมาตรฐาน$@อาร์เรย์หรือเพื่อสร้างฟังก์ชันแยกต่างหากสำหรับสิ่งนี้ ชี้ว่ามันอาจเป็นวิธีที่ง่ายที่สุด)

ชี้แจง: วัตถุคือการปรับเปลี่ยนค่าของความยาวตัวแปร $@เพื่อให้รหัสง่ายต่อการตรวจแก้จุดบกพร่อง เวอร์ชันปัจจุบันค่อนข้างแฮ็คเกินไปสำหรับความชอบของฉันแม้ว่ามันจะทำงานได้แม้ในเส้นทางที่แปลกประหลาดเช่น

$'--$`\! *@ \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

คำตอบ:


16

POSIX

ในการทำให้สแลชของพารามิเตอร์ทั้งหมดเป็นปกติฉันจะใช้เคล็ดลับการโต้แย้งแบบหมุน: $1ปิดเปลี่ยนรูปแล้วนำผลลัพธ์ที่ส่วนท้ายของรายการพารามิเตอร์ หากคุณทำเช่นนั้นหลายครั้งตามที่มีพารามิเตอร์คุณจะเปลี่ยนพารามิเตอร์ทั้งหมดและคุณจะได้รับกลับมาตามลำดับ

สำหรับส่วนที่สองของรหัสฉันเปลี่ยนลอจิกของคุณให้สับสนน้อยลง: ลูปด้านนอกวนซ้ำพารามิเตอร์และลูปด้านในจะวนซ้ำไปตามส่วนประกอบของพา ธ for x; do … doneวนซ้ำพารามิเตอร์ตำแหน่งมันเป็นสำนวนที่สะดวก ฉันใช้วิธีที่เข้ากันได้กับ POSIX ในการจับคู่สตริงกับรูปแบบ: caseโครงสร้าง

ทดสอบด้วยเส้นประ 0.5.5.1, pdksh 5.2.14, ทุบตี 3.2.39, ทุบตี 4.1.5, ksh 93s +, zsh 4.3.10

หมายเหตุด้านข้าง: ดูเหมือนว่าจะมีข้อผิดพลาดในทุบตี 4.1.5 (ไม่ได้อยู่ใน 3.2): หากรูปแบบกรณี"${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"
}

ทุบตี, ksh

หากคุณกำลังทุบตี (หรือ ksh) คุณสามารถใช้อาร์เรย์ - ฉันไม่เข้าใจว่าทำไมคุณถึงดูเหมือนจะ จำกัด ตัวเองกับพารามิเตอร์ตำแหน่ง นี่คือรุ่นที่ใช้อาร์เรย์ ฉันต้องยอมรับว่ามันไม่ได้ชัดเจนกว่า POSIX รุ่น แต่มันจะหลีกเลี่ยงการสับเริ่มต้น n ^ 2

สำหรับส่วนเฉือนฟื้นฟูผมใช้ ksh93 สร้าง${foo//PATTERN/REPLACEMENT}โครงสร้างที่จะแทนที่เกิดขึ้นทั้งหมดPATTERNในโดย$foo REPLACEMENTรูปแบบคือ+(\/)การจับคู่หนึ่งหรือมากกว่าหนึ่งทับ ภายใต้ 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"
}

zsh

น่าเสียดายที่ 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 แล้วตัดมันออกในบรรทัดถัดไปหรือไม่ ดูเหมือนว่ารหัสจะทำงานไม่ได้ แต่ฉันคิดว่าคุณกำลังจัดการกับกรณีที่ฉันไม่รู้
Alan De Smet

1
@AlanDeSmet ตัวพิมพ์ขอบคือถ้าสตริงลงท้ายด้วย newline การทดแทนคำสั่งตัดการขึ้นบรรทัดใหม่ต่อท้าย
Gilles 'หยุดความชั่วร้าย'

6

ทำไมคุณไม่ใช้ $ 1, $ 2 .. $ 9, $ {10}, $ {11} .. และอื่น ๆ มันยิ่งแห้งกว่าที่คุณต้องการทำ :)

เพิ่มเติมเกี่ยวกับความสัมพันธ์ระหว่าง $ numberและ $ @:

$ @ ถือได้ว่าเป็นชวเลขสำหรับ "องค์ประกอบทั้งหมดของอาร์เรย์ที่มีอาร์กิวเมนต์ทั้งหมด"

ดังนั้น $ @ เป็นชวเลขเรียงลำดับของ $ {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

ข้อเสียที่เห็นได้ชัดของการมีการใช้ $ * * * * * * * * ${args[$i]}จำนวนคือการที่คุณไม่สามารถใช้ตัวแปรดัชนีเช่นเดียวกับ
intuited

@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

@epoluan ... ขอบคุณที่แจ้งให้ฉันทราบ มันง่ายกว่าที่จะเขียน ... (ตอนนี้ฉันกลับไปที่หน้าเว็บที่ฉันอ้างถึงถ้าฉันอ่านเพิ่มเติมฉันจะได้เห็นมันพูดถึงมันด้วย :( ....
Peter.O

หึ แต่ถ้าการอ้อมเกิดขึ้นทางด้านซ้าย Eval เป็นความชั่วร้ายที่จำเป็น Tho ':)
pepoluan

@ peopluan ... โอเคขอบคุณที่ชี้ให้เห็นว่า ... และแยกออกจากกัน: ฉันไม่เข้าใจว่าทำไมevalบางคนถึงได้รับการพิจารณาให้เป็นevil... (อาจเป็นเพราะการสะกด :) หากeval"ไม่ดี" จะเท่ากับ $ {! var} เท่ากัน "ไม่ดี" หรือไม่ ... สำหรับฉันมันเป็นเพียงส่วนหนึ่งของภาษาและเป็นส่วนที่มีประโยชน์ในตอนนั้น .. แต่ฉันชอบ $ {! var} ...
Peter.O

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
}
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.