Mengindeks dan memodifikasi array parameter Bash $ @

11

Apakah mungkin untuk merujuk ke indeks $@? Saya tidak dapat menemukan referensi untuk digunakan seperti di bagian mana saja di wiki GrayCat , dan Advanced Scripting Guide dan yang lainnya memberikan ini pada variabel yang berbeda sebelum mengubahnya.

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

Tujuannya KERING : Argumen pertama digunakan untuk satu hal, dan sisanya untuk sesuatu yang lain, dan saya ingin menghindari duplikasi kode untuk menormalkan, $@array, atau untuk membuat fungsi terpisah untuk ini (walaupun saat ini titik itu mungkin jalan keluar termudah).

Klarifikasi: Objeknya adalah untuk memodifikasi nilai-nilai dari panjang variabel $@ untuk membuat kode lebih mudah untuk di-debug. Versi saat ini agak terlalu hacky untuk seleraku, meskipun itu berfungsi bahkan untuk jalur aneh seperti

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

Pembaruan : Sepertinya ini tidak mungkin. Kode sekarang menggunakan duplikasi kode dan data, tetapi setidaknya berfungsi:

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 pergi ke siapa saja yang dapat menyingkirkan duplikasi kode untuk memangkas duplikasi garis miring atau duplikasi data untuk disimpan $1dan parameter lainnya, atau keduanya, sambil menjaga kode ukuran yang masuk akal dan berhasil semua unit test:

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
l0b0
sumber
terkait: stackoverflow.com/questions/4827690/…
Ciro Santilli 冠状 病毒 审查 六四 六四 事件 法轮功

Jawaban:

16

POSIX

Untuk menormalkan garis miring pada semua parameter, saya akan menggunakan trik argumen rotating: shift $1off, transform, dan letakkan hasilnya di akhir daftar parameter. Jika Anda melakukannya sebanyak waktu karena ada parameter, Anda telah mengubah semua parameter, dan Anda mendapatkannya kembali secara berurutan.

Untuk bagian kedua dari kode, saya mengubah logika Anda menjadi kurang membingungkan: loop luar iterates atas parameter, dan loop dalam iterate di atas komponen path. for x; do … doneiterates atas parameter posisi, itu adalah ungkapan yang nyaman. Saya menggunakan cara yang sesuai dengan POSIX untuk mencocokkan string dengan sebuah pola: casekonstruk.

Diuji dengan tanda hubung 0.5.5.1, pdksh 5.2.14, bash 3.2.39, bash 4.1.5, ksh 93s +, zsh 4.3.10.

Catatan: sepertinya ada bug di bash 4.1.5 (bukan di 3.2): jika pola case-nya "${common_path%/}"/*, salah satu tes gagal.

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

Jika Anda menggunakan bash (atau ksh), Anda dapat menggunakan array - Saya tidak mengerti mengapa Anda tampaknya membatasi diri Anda ke parameter posisi. Berikut adalah versi yang menggunakan array. Saya harus mengakui itu tidak terlalu jelas dari versi POSIX, tetapi ia menghindari pengocokan awal n ^ 2.

Untuk bagian slash normalisasi, saya menggunakan ksh93 membangun ${foo//PATTERN/REPLACEMENT}konstruk mengganti semua kejadian dari PATTERNdalam $foooleh REPLACEMENT. Polanya adalah +(\/)mencocokkan satu atau lebih garis miring; di bawah bash, shopt -s extglobharus berlaku (setara, mulai bash dengan bash -O extglob). Konstruk set ${!a[@]}menetapkan parameter posisi ke daftar subkrip array a. Ini memberikan cara mudah untuk beralih pada elemen-elemen array.

Untuk bagian kedua, saya logika loop yang sama dengan versi POSIX. Kali ini, saya dapat menggunakan [[ … ]]karena semua shell yang ditargetkan di sini mendukungnya.

Diuji dengan 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

Sayangnya, zsh tidak memiliki ${!array[@]}fitur untuk menjalankan versi ksh93 apa adanya. Untungnya, zsh memiliki dua fitur yang membuat bagian pertama mudah. Anda bisa mengindeks parameter posisi seolah-olah mereka adalah @array, jadi tidak perlu menggunakan array perantara. Dan zsh memiliki susunan iterasi susunan : "${(@)array//PATTERN/REPLACEMENT}"melakukan penggantian pola pada setiap elemen susunan secara bergantian dan mengevaluasi susunan hasil (membingungkan, Anda memang membutuhkan tanda kutip ganda walaupun hasilnya beberapa kata; ini adalah generalisasi dari "$@"). Bagian kedua pada dasarnya tidak berubah.

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

Uji kasus

Solusi saya minimal diuji dan dikomentari. Saya telah mengubah sintaks kasus uji Anda untuk menguraikan di bawah shell yang tidak memiliki $'…'dan melaporkan kegagalan dengan cara yang lebih nyaman.

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
}
Gilles 'SANGAT berhenti menjadi jahat'
sumber
1
+50, wow. Lebih dari yang saya minta, bagaimanapun juga. Anda, tuan, luar biasa.
l0b0
Dalam diskusi POSIX, di loop pertama tempat Anda menormalkan tanda garis, mengapa menambahkan "." dengan sprintf lalu lepas di baris berikutnya? Kode tampaknya berfungsi tanpa itu, tapi saya curiga Anda sedang menangani kasus tepi saya tidak menyadarinya.
Alan De Smet
1
@AlanDeSmet Kasus tepi adalah jika string berakhir dengan baris baru. Perintah penggantian strip off trailing baris baru.
Gilles 'SANGAT berhenti menjadi jahat'
6

Mengapa Anda tidak menggunakan $ 1, $ 2 .. $ 9, $ {10}, $ {11} .. dan seterusnya? Ini bahkan lebih KERING- daripada apa yang Anda coba lakukan :)

Lebih lanjut tentang hubungan antara $ number dan $ @:

$ @ dapat dianggap sebagai singkatan untuk "semua elemen dari array yang berisi semua argumen"

Jadi, $ @ adalah semacam singkatan dari $ {args [@]} (args di sini adalah array 'virtual' yang berisi semua argumen - bukan variabel nyata, ingatlah)

$ 1 adalah $ {args [1]}, $ 2 adalah $ {args [2]}, dan seterusnya.

Saat Anda menekan [9], gunakan tanda kurung: $ {10} adalah $ {args [10]}, $ {11} adalah $ {args [11]}, dan seterusnya.


Secara tidak langsung menggunakan argumen baris perintah

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

Contoh:

argc=$#
for (( argn=1; argn<=argc; argn++)); do
    if [[ ${!argn} == "foo" ]]; then
        echo "Argument $argn of $argc is 'foo'"
    fi
done
pepoluan
sumber
Kelemahan yang jelas dari keharusan menggunakan $ * number * adalah Anda tidak dapat menggunakan variabel indeks seperti ${args[$i]}.
intuited
@intuited lalu gunakan tipuan; Saya akan mengedit jawaban saya.
pepoluan
5

Argumen pertama digunakan untuk satu hal, dan sisanya untuk sesuatu yang lain,

Saya pikir apa yang Anda inginkan 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
forcefsck
sumber
1

Saya tidak tahu mengapa Anda tidak hanya menggunakan $ 1 $ 2, dll. Tapi .. Ini mungkin sesuai dengan kebutuhan Anda.

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

keluaran

  -------- 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 karya apa pun yang mengikutinya, untuk membuat $ 1, $ 2 .. dll. Ini tentu saja akan mengesampingkan nilai-nilai asli, jadi hanya perlu diketahui itu.

Peter.O
sumber
ahh ... jadi dengan 'eval' maksud Anda referensi tidak langsung ... $ {! var} konstruk lebih aman, seperti apa yang saya tulis dalam jawaban saya
pepoluan
@pepoluan ... Terima kasih sudah mengingatkan saya akan hal itu. Jauh lebih mudah untuk menulis ... (Saya baru saja kembali ke halaman web yang saya
maksudkan
heh. tetapi jika tipuan terjadi di sisi kiri , eval adalah kejahatan yang diperlukan, tho ':)
pepoluan
@peopluan ... oke, terima kasih sudah menunjukkannya ... dan sebagai tambahan: saya tidak mengerti mengapa evaldianggap oleh sebagian orang sebagai evil... (mungkin karena ejaannya :) ... Jika eval"buruk", apakah $ {! Var} sama dengan "buruk"? ... Bagi saya itu hanya bagian dari bahasa, dan bagian yang berguna, pada saat itu .. tapi saya lebih suka $ {! Var} ...
Peter.O
1

Catatan saya mendukung spasi dalam nama file.

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

Saya menambahkan kasus uji untuk nama file dengan spasi dan memperbaiki 2 tes yang tidak ada yang memimpin /

    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
}
John Kearney
sumber