Cara mendapatkan argumen terakhir ke fungsi / bin / sh

11

Apa cara yang lebih baik untuk diterapkan print_last_arg?

#!/bin/sh

print_last_arg () {
    eval "echo \${$#}"  # this hurts
}

print_last_arg foo bar baz
# baz

(Jika ini, katakanlah, #!/usr/bin/zshalih-alih #!/bin/shsaya tahu apa yang harus dilakukan. Masalah saya adalah menemukan cara yang masuk akal untuk mengimplementasikannya #!/bin/sh.)

EDIT: Di atas hanyalah contoh konyol. Tujuan saya bukan untuk mencetak argumen terakhir, tetapi untuk memiliki cara untuk merujuk ke argumen terakhir dalam fungsi shell.


EDIT2: Saya minta maaf atas pertanyaan yang tidak jelas. Saya berharap bisa melakukannya dengan benar kali ini.

Jika ini /bin/zshbukan /bin/sh, saya bisa menulis sesuatu seperti ini

#!/bin/zsh

print_last_arg () {
    local last_arg=$argv[$#]
    echo $last_arg
}

Ekspresi $argv[$#]adalah contoh dari apa yang saya jelaskan di EDIT pertama saya sebagai cara untuk merujuk ke argumen terakhir dalam fungsi shell .

Karena itu, saya seharusnya menulis contoh asli saya seperti ini:

print_last_arg () {
    local last_arg=$(eval "echo \${$#}")   # but this hurts even more
    echo $last_arg
}

... untuk memperjelas bahwa apa yang saya kejar adalah hal yang kurang mengerikan untuk diletakkan di sebelah kanan penugasan.

Namun, perhatikan bahwa dalam semua contoh, argumen terakhir diakses secara non-destruktif . TKI, mengakses argumen terakhir membuat argumen posisi secara keseluruhan tidak terpengaruh.

kjo
sumber
unix.stackexchange.com/q/145522/117549 menunjukkan berbagai kemungkinan #! / bin / sh - dapatkah Anda mempersempitnya?
Jeff Schaller
saya memang menyukai suntingan, tetapi mungkin Anda mungkin juga memperhatikan bahwa ada jawaban di sini yang menawarkan cara tidak merusak untuk merujuk argumen terakhir ...? Anda tidak harus menyamakan var=$( eval echo \${$#})ke eval var=\${$#}- keduanya tidak sama.
mikeserv
1
Tidak yakin saya mendapatkan catatan terakhir Anda, tetapi hampir semua jawaban yang diberikan sejauh ini tidak merusak dalam arti mereka mempertahankan argumen skrip yang sedang berjalan. Hanya shiftdan set -- ...solusi berbasis mungkin merusak kecuali digunakan dalam fungsi di mana mereka juga tidak berbahaya.
jlliagre
@ jlliagre - tetapi mereka masih merusak pada utamanya - mereka membutuhkan menciptakan konteks pakai sehingga mereka dapat menghancurkan untuk menemukan. tetapi ... jika Anda mendapatkan konteks kedua - mengapa tidak hanya mendapatkan satu memungkinkan Anda untuk mengindeks? apakah ada yang salah dengan menggunakan alat yang ditujukan untuk pekerjaan itu? menafsirkan ekspansi shell sebagai input yang dapat diperluas adalah tugas eval. dan tidak ada yang berbeda secara signifikan eval "var=\${$#}"jika dilakukan dibandingkan dengan var=${arr[evaled index]}kecuali itu $#adalah nilai aman yang dijamin. mengapa menyalin seluruh set lalu menghancurkannya ketika Anda bisa langsung mengindeksnya?
mikeserv
1
@ mikeserv A untuk loop yang dilakukan di bagian utama shell adalah membiarkan semua argumen tidak berubah. Saya setuju mengulang semua argumen sangat tidak dioptimalkan, terutama jika ribuan dari mereka dilewatkan ke shell dan saya setuju juga bahwa secara langsung mengakses argumen terakhir dengan indeks yang tepat adalah jawaban terbaik (dan saya tidak mengerti mengapa argumen itu dibatalkan) ) tetapi lebih dari itu, tidak ada yang benar-benar merusak dan tidak ada konteks tambahan yang dibuat.
jlliagre

Jawaban:

1
eval printf %s${1+"'\n' \"the last arg is \${$#"\}\"}

... akan mencetak string yang the last arg isdiikuti oleh <spasi> , nilai argumen terakhir, dan <newline> tertinggal jika ada setidaknya 1 argumen, atau jika tidak, untuk argumen nol, tidak akan mencetak apa pun.

Jika Anda melakukannya:

eval ${1+"lastarg=\${$#"\}}

... maka Anda akan menetapkan nilai argumen terakhir ke variabel shell $lastargjika ada setidaknya 1 argumen, atau Anda tidak akan melakukan apa pun. Either way, Anda akan melakukannya dengan aman, dan itu harus portabel bahkan untuk kamu Olde Bourne shell, saya pikir.

Berikut ini satu lagi yang akan bekerja dengan cara yang sama, meskipun itu perlu menyalin seluruh array arg dua kali (dan memerlukan printfin $PATHuntuk shell Bourne) :

if   [ "${1+:}" ]
then set "$#" "$@" "$@"
     shift    "$1"
     printf %s\\n "$1"
     shift
fi
mikeserv
sumber
Komentar bukan untuk diskusi panjang; percakapan ini telah dipindahkan ke obrolan .
terdon
2
FYI, saran pertama Anda gagal di bawah legacy bourne shell dengan kesalahan "substitusi buruk" jika tidak ada argumen. Keduanya tetap berfungsi seperti yang diharapkan.
jlliagre
@ jillagre - terima kasih. Saya tidak begitu yakin tentang yang teratas - tetapi cukup yakin dengan dua lainnya. itu hanya dimaksudkan untuk menunjukkan bagaimana argumen dapat diakses dengan referensi inline. lebih disukai suatu fungsi akan terbuka dengan sesuatu seperti ${1+":"} returnsegera - karena siapa yang menginginkannya melakukan sesuatu atau mempertaruhkan efek samping apa pun jika dipanggil tanpa alasan? Matematika cukup untuk hal yang sama - jika Anda yakin Anda dapat memperluas bilangan bulat positif, Anda dapat melakukannya evalsepanjang hari. $OPTINDbagus untuk itu.
mikeserv
3

Berikut cara sederhananya:

print_last_arg () {
  if [ "$#" -gt 0 ]
  then
    s=$(( $# - 1 ))
  else
    s=0
  fi
  shift "$s"
  echo "$1"
}

(diperbarui berdasarkan titik @ cuonglm bahwa dokumen asli gagal ketika tidak ada argumen; ini sekarang menggemakan baris kosong - ubah perilaku itu dalam elseklausa jika diinginkan)

Jeff Schaller
sumber
Ini diperlukan menggunakan / usr / xpg4 / bin / sh di Solaris; beri tahu saya jika Solaris #! / bin / sh adalah persyaratan.
Jeff Schaller
Pertanyaan ini bukan tentang skrip tertentu yang saya coba tulis, tetapi lebih pada bagaimana umum. Saya mencari resep yang bagus untuk ini. Tentu saja, semakin portabel, semakin baik.
kjo
matematika $ (()) gagal di Solaris / bin / sh; gunakan sesuatu seperti: shift `expr $ # - 1` jika itu diperlukan.
Jeff Schaller
sebagai alternatif untuk Solaris / bin / sh,echo "$# - 1" | bc
Jeff Schaller
1
itulah tepatnya yang ingin saya tulis, tetapi gagal pada shell ke-2 yang saya coba - NetBSD, yang ternyata adalah shell Almquist yang tidak mendukungnya :(
Jeff Schaller
3

Diberikan contoh pos pembuka (argumen posisi tanpa spasi):

print_last_arg foo bar baz

Untuk standarnya IFS=' \t\n', bagaimana dengan:

args="$*" && printf '%s\n' "${args##* }"

Untuk perluasan yang lebih aman "$*", atur IFS (per @ StéphaneChazelas):

( IFS=' ' args="$*" && printf '%s\n' "${args##* }" )

Tetapi hal di atas akan gagal jika argumen posisi Anda dapat berisi spasi. Dalam hal ini, gunakan ini sebagai gantinya:

for a in "$@"; do : ; done && printf '%s\n' "$a"

Perhatikan bahwa teknik ini menghindari penggunaan evaldan tidak memiliki efek samping.

Diuji di shellcheck.net

AsymLabs
sumber
1
Yang pertama gagal jika argumen terakhir berisi ruang.
cuonglm
1
Perhatikan bahwa yang pertama juga tidak bekerja di shell pra-POSIX
cuonglm
@cuonglm Terlihat dengan baik, pengamatan Anda yang benar sudah tergabung sekarang.
AsymLabs
Ini juga mengasumsikan karakter pertama $IFSadalah spasi.
Stéphane Chazelas
1
Itu akan terjadi jika tidak ada $ IFS di lingkungan, tidak ditentukan sebaliknya. Tetapi karena Anda perlu mengatur IFS secara virtual setiap kali Anda menggunakan operator split + glob (biarkan tanda kutip ekspansi), salah satu pendekatan untuk menangani IFS adalah mengaturnya kapan pun Anda membutuhkannya. Tidak ada ruginya memiliki di IFS=' 'sini hanya untuk memperjelas bahwa itu digunakan untuk ekspansi "$*".
Stéphane Chazelas
3

Meskipun pertanyaan ini baru berumur lebih dari 2 tahun, saya pikir saya akan berbagi opsi yang agak compacter.

print_last_arg () {
    echo "${@:${#@}:${#@}}"
}

Mari kita jalankan

print_last_arg foo bar baz
baz

Perluasan parameter shell bash .

Edit

Yang lebih ringkas: echo "${@: -1}"

(Pikirkan ruang)

Sumber

Diuji pada macOS 10.12.6 tetapi juga harus mengembalikan argumen terakhir pada kebanyakan rasa * nix yang tersedia ...

Sakitnya jauh lebih sedikit ¯\_(ツ)_/¯

pengguna2243670
sumber
Ini harus menjadi jawaban yang diterima. Bahkan lebih baik: echo "${*: -1}"yang shellchecktidak akan mengeluh.
Tom Hale
4
Ini tidak akan berfungsi dengan POSIX sh, ${array:n:m:}adalah ekstensi. (pertanyaan itu secara eksplisit menyebutkan /bin/sh)
ilkkachu
2

POSIXly:

while [ "$#" -gt 1 ]; do
  shift
done

printf '%s\n' "$1"

(Pendekatan ini juga bekerja di shell Bourne lama)

Dengan alat standar lainnya:

awk 'BEGIN{print ARGV[ARGC-1]}' "$@"

(Ini tidak akan bekerja dengan yang lama awk, yang tidak memiliki ARGV)

cuonglm
sumber
Di mana masih ada kulit Bourne, awkbisa juga menjadi awk tua yang tidak punya ARGV.
Stéphane Chazelas
@ StéphaneChazelas: Setuju. Setidaknya itu bekerja dengan versi Brian Kernighan sendiri. Apakah Anda punya cita-cita?
cuonglm
Ini menghapus argumen. Bagaimana cara menyimpannya?
@ Binzebra: Gunakan awkpendekatan, atau gunakan forloop biasa seperti yang lainnya, atau meneruskannya ke fungsi.
cuonglm
2

Ini harus bekerja dengan setiap shell yang sesuai dengan POSIX dan akan bekerja juga dengan shell Solaris Bourne pre POSIX sebelumnya:

do=;for do do :;done;printf "%s\n" "$do"

dan ini adalah fungsi berdasarkan pendekatan yang sama:

print_last_arg()
  if [ "$*" ]; then
    for do do :;done
    printf "%s\n" "$do"
  else
    echo
  fi

PS: jangan bilang saya lupa kurung kurawal di sekitar fungsi tubuh ;-)

Jlliagre
sumber
2
Anda lupa kawat gigi keriting di sekitar fungsi tubuh ;-).
@ Binzebra saya memperingatkan Anda ;-) Saya tidak melupakan mereka. Kawat gigi secara mengejutkan opsional di sini.
jlliagre
1
@ jlliagre I, memang, diperingatkan ;-): P ...... Dan: tentu saja!
Bagian mana dari spesifikasi sintaks yang memungkinkan ini?
Tom Hale
@ TomHale Aturan tata bahasa shell memungkinkan kedua hilang in ...dalam satu forlingkaran dan tidak ada kurung kurawal di sekitar ifpernyataan yang digunakan sebagai fungsi tubuh. Lihat for_clausedan compound_commanddi pubs.opengroup.org/onlinepubs/9699919799 .
jlliagre
2

Dari "Unix - Pertanyaan yang Sering Diajukan"

(1)

unset last
if    [ $# -gt 0 ]
then  eval last=\${$#}
fi
echo  "$last"

Jika jumlah argumen bisa nol, maka argumen nol $0(biasanya nama skrip) akan ditetapkan $last. Itulah alasannya jika.

(2)

unset last
for   last
do    :
done
echo  "$last"

(3)

for     i
do
        third_last=$second_last
        second_last=$last
        last=$i
done
echo    "$last"

Untuk menghindari pencetakan baris kosong ketika tidak ada argumen, ganti echo "$last"untuk:

${last+false} || echo "${last}"

Jumlah argumen nol dihindari oleh if [ $# -gt 0 ].

Ini bukan salinan persis dari apa yang ada di tautan di halaman, beberapa perbaikan ditambahkan.


sumber
0

Ini harus bekerja pada semua sistem dengan perldiinstal (sehingga sebagian besar UNICES):

print_last_arg () {
    printf '%s\0' "$@" | perl -0ne 's/\0//;$l=$_;}{print "$l\n"'
}

Caranya adalah dengan menggunakan printfmenambahkan \0setelah setiap argumen shell dan kemudian perl's -0saklar yang menetapkan pemisah rekor ke NULL. Kemudian kita beralih pada input, menghapus \0dan menyimpan setiap baris yang diakhiri NULL sebagai $l. The ENDblok (yang ini apa }{yang) akan dilaksanakan setelah semua input telah membaca begitu akan mencetak yang terakhir "garis": yang terakhir shell argumen.

terdon
sumber
@ mikeserv ah, benar, saya belum menguji dengan baris baru dalam argumen terakhir. Versi yang diedit seharusnya bisa digunakan untuk hampir semua hal tetapi mungkin terlalu berbelit-belit untuk tugas yang begitu sederhana.
terdon
ya, memanggil sebuah executable luar (jika itu belum menjadi bagian dari logika) , bagi saya, sedikit berat. tetapi jika intinya adalah untuk lulus argumen bahwa pada sed, awkatau perlmaka bisa menjadi informasi yang berharga pula. Anda masih dapat melakukan hal yang sama dengan sed -zuntuk versi GNU terbaru.
mikeserv
mengapa Anda downvoted? ini bagus ... saya pikir?
mikeserv
0

Ini adalah versi yang menggunakan rekursi. Tidak tahu bagaimana ini sesuai dengan POSIX ...

print_last_arg()
{
    if [ $# -gt 1 ] ; then
        shift
        echo $( print_last_arg "$@" )
    else
        echo "$1"
    fi
}
brm
sumber
0

Konsep sederhana, tanpa aritmatika, tanpa loop, tanpa eval , hanya fungsi.
Ingat bahwa cangkang Bourne tidak memiliki aritmatika (diperlukan eksternal expr). Jika ingin mendapatkan aritmatika gratis, evalpilihan bebas, ini adalah pilihan. Membutuhkan fungsi berarti SVR3 atau lebih tinggi (tidak ada parameter yang ditimpa).
Lihat di bawah untuk versi yang lebih kuat dengan printf.

printlast(){
    shift "$1"
    echo "$1"
}

printlast "$#" "$@"          ### you may use ${1+"$@"} here to allow
                             ### a Bourne empty list of arguments,
                             ### but wait for a correct solution below.

Struktur ini untuk panggilan printlastadalah tetap , argumen perlu ditetapkan dalam daftar argumen shell $1, $2dll (argumen stack) dan panggilan dilakukan seperti yang diberikan.

Jika daftar argumen perlu diubah, tetapkan saja:

set -- o1e t2o t3r f4u
printlast "$#" "$@"

Atau buat fungsi yang lebih mudah digunakan ( getlast) yang bisa memungkinkan argumen umum (tapi tidak secepat, argumen dilewatkan dua kali).

getlast(){ printlast "$#" "$@"; }
getlast o1e t2o t3r f4u

Harap dicatat bahwa argumen (dari getlast, atau semua yang termasuk dalam $@printlast) dapat memiliki spasi, baris baru, dll. Tetapi tidak NUL.

Lebih baik

Versi ini tidak mencetak 0ketika daftar argumen kosong, dan menggunakan printf yang lebih kuat (mundur ke echojika eksternal printftidak tersedia untuk shell lama).

printlast(){ shift  "$1"; printf '%s' "$1"; }
getlast  ()  if     [ $# -gt 0 ]
             then   printlast "$#" "$@"
                    echo    ### optional if a trailing newline is wanted.
             fi
### The {} braces were removed on purpose.

getlast 1 2 3 4    # will print 4

Menggunakan EVAL.

Jika shell Bourne lebih tua dan tidak ada fungsi, atau jika karena alasan tertentu menggunakan eval adalah satu-satunya pilihan:

Untuk mencetak nilai:

if    [ $# -gt 0 ]
then  eval printf "'1%s\n'" \"\$\{$#\}\"
fi

Untuk menetapkan nilai dalam variabel:

if    [ $# -gt 0 ]
then  eval 'last_arg='\"\$\{$#\}\"
fi

Jika itu perlu dilakukan dalam suatu fungsi, argumen harus disalin ke fungsi:

print_last_arg () {
    local last_arg                ### local only works on more modern shells.
    eval 'last_arg='\"\$\{$#\}\"
    echo "last_arg3=$last_arg"
}

print_last_arg "$@"               ### Shell arguments are sent to the function.

sumber
itu lebih baik tetapi dikatakan tidak menggunakan loop - tetapi itu tidak. salinan array adalah sebuah loop . dan dengan dua fungsi ini menggunakan dua loop. juga - apakah ada alasan iterating seluruh array harus diindeks dengan eval?
mikeserv
1
Apakah Anda melihat for loopatau while loop?
ya saya lakukan
mikeserv
1
Dengan standar Anda, saya kira semua kode memiliki loop, bahkan echo "Hello"memiliki loop karena CPU harus membaca lokasi memori dalam satu lingkaran. Lagi: kode saya tidak memiliki loop tambahan.
1
Saya benar-benar tidak peduli dengan pendapat Anda tentang baik atau buruk, sudah terbukti salah beberapa kali. Lagi: kode saya tidak memiliki for, whileatau untilloop. Apakah Anda melihat for whileatau untilloop tertulis dalam kode? Tidak ?, maka terimalah fakta, rangkul dan pelajari.
0

Komentar ini menyarankan untuk menggunakan kata yang sangat elegan:

echo "${@:$#}"
Tom Hale
sumber