bash cara menghapus opsi dari parameter setelah pemrosesan

9

Saya ingat pernah melihat di suatu tempat bashskrip menggunakan casedan shiftberjalan melalui daftar parameter posisi, mengurai flag dan opsi dengan argumen ketika bertemu mereka, dan menghapusnya setelah parsing hanya meninggalkan argumen kosong, yang kemudian diproses oleh sisa dari naskah.

Misalnya, dalam mem-parsing baris perintah cp -R file1 -t /mybackup file2 -f, pertama-tama ia akan berjalan melalui parameter, mengenali bahwa pengguna telah meminta untuk turun ke direktori dengan -R, menentukan target dengan -t /mybackupdan untuk memaksa menyalin -f, dan menghapus yang dari daftar parameter, meninggalkan program untuk diproses file1 file2sebagai argumen yang tersisa.

Tapi sepertinya saya tidak bisa mengingat / mencari tahu naskah apa pun yang saya lihat kapan saja. Saya hanya ingin bisa melakukan itu. Saya telah googling di berbagai situs dan menambahkan daftar halaman yang relevan yang saya periksa.

Sebuah pertanyaan di situs web ini secara khusus bertanya tentang "opsi tidak tergantung pesanan" tetapi baik jawaban tunggal maupun jawaban pertanyaan yang ditiru tidak mempertimbangkan kasus-kasus seperti di atas di mana opsi dicampur dengan argumen normal, yang saya anggap sebagai alasan bagi orang tersebut untuk secara spesifik menyebutkan opsi pesanan-independen .

Karena bashbuilt-in getoptstampaknya berhenti pada argumen non-opsi pertama, sepertinya tidak cukup sebagai solusi. Inilah sebabnya mengapa halaman Wooledge BashFAQ (lihat di bawah) menjelaskan cara mengatur ulang argumen. Tetapi saya ingin menghindari membuat banyak array jika daftar argumennya cukup panjang.

Karena shifttidak mendukung memunculkan argumen individual di bagian tengah daftar parameter, saya tidak yakin apa cara langsung untuk mengimplementasikan apa yang saya minta.

Saya ingin mendengar jika ada yang punya solusi untuk menghapus argumen dari tengah daftar parameter tanpa membuat array baru.

Halaman yang sudah saya lihat:

jamadagni
sumber
1
Setelah saya membutuhkan sesuatu yang rumit, saya beralih dari bash ke Perl.
choroba

Jawaban:

9

POSIXly, penguraian untuk opsi harus berhenti pada --atau pada argumen non-opsi (atau non-opsi-argumen) pertama yang mana yang lebih dulu. Jadi masuk

cp -R file1 -t /mybackup file2 -f

yang ini di file1, jadi cpharus rekursif menyalin semua file1, -t, /mybackupdan file2ke dalam -fdirektori.

getopt(3)Namun, GNU (yang cpdigunakan GNU untuk menguraikan opsi (dan di sini Anda menggunakan GNU cpkarena Anda menggunakan -topsi spesifik GNU )), kecuali $POSIXLY_CORRECTvariabel lingkungan diatur, menerima opsi setelah argumen. Jadi itu sebenarnya setara dengan parsing gaya opsi POSIX:

cp -R -t /mybackup -f -- file1 file2

The getoptsshell built-in, bahkan di GNU shell ( bash) hanya menangani gaya POSIX. Itu juga tidak mendukung opsi panjang atau opsi dengan argumen opsional.

Jika Anda ingin menguraikan opsi dengan cara yang sama seperti GNU cp, Anda harus menggunakan getopt(3)API GNU . Untuk itu, jika di Linux, Anda dapat menggunakan getoptutilitas yang disempurnakan dari util-linux( versi yang disempurnakan dari getoptperintah tersebut juga telah porting ke beberapa Unix lain seperti FreeBSD ).

Itu getoptakan mengatur ulang opsi dengan cara kanonik yang memungkinkan Anda untuk menguraikannya hanya dengan satu while/caselingkaran.

$ getopt -n "$0" -o t:Rf -- -Rf file1 -t/mybackup file2
 -R -f -t '/mybackup' -- 'file1' 'file2'

Anda biasanya menggunakannya sebagai:

parsed_options=$(
  getopt -n "$0" -o t:Rf -- "$@"
) || exit
eval "set -- $parsed_options"
while [ "$#" -gt 0 ]; do
  case $1 in
    (-[Rf]) shift;;
    (-t) shift 2;;
    (--) shift; break;;
    (*) exit 1 # should never be reached.
  esac
done
echo "Now, the arguments are $*"

Perhatikan juga bahwa itu getoptakan mem-parsing opsi dengan cara yang sama seperti GNU cp. Secara khusus, ini mendukung opsi panjang (dan memasukkannya disingkat) dan menghargai $POSIXLY_CORRECTvariabel lingkungan (yang ketika disetel menonaktifkan dukungan untuk opsi setelah argumen) dengan cara yang sama seperti GNU cp.

Perhatikan bahwa menggunakan gdb dan mencetak argumen yang getopt_long()menerima dapat membantu membangun parameter ke getopt(1):

(gdb) bt
#0  getopt_long (argc=2, argv=0x7fffffffdae8, options=0x4171a6 "abdfHilLnprst:uvxPRS:T", long_options=0x417c20, opt_index=0x0) at getopt1.c:64
(gdb) set print pretty on
(gdb) p *long_options@40
$10 = {{
    name = 0x4171fb "archive",
    has_arg = 0,
    flag = 0x0,
    val = 97
  }, {
    name = 0x417203 "attributes-only",
[...]

Maka Anda dapat menggunakan getoptsebagai:

getopt -n cp -o abdfHilLnprst:uvxPRS:T -l archive... -- "$@"

Ingatlah bahwa cpdaftar opsi yang didukung GNU dapat berubah dari satu versi ke versi berikutnya dan yang getopttidak akan dapat memeriksa apakah Anda memberikan nilai legal ke --sparseopsi misalnya.

Stéphane Chazelas
sumber
@all: terima kasih atas balasan Anda. @Stephane: apakah ada alasan Anda menggunakan while [ "$#" -gt 0 ]io while (($#))? Apakah hanya untuk menghindari bashism?
jamadagni
Ya, meskipun (($#))lebih kshism .
Stéphane Chazelas
1

Jadi setiap kali getoptsmemproses argumen, ia tidak berharap itu menetapkan variabel shell $OPTINDke nomor berikutnya dalam daftar argumen yang harus diproses dan mengembalikan selain 0. Jika $OPTINDdiatur ke nilai 1, getoptsditentukan POSIX untuk menerima daftar argumen baru. Jadi ini hanya menonton getoptskembali, menyimpan kenaikan counter plus $OPTINDuntuk setiap pengembalian yang gagal, menggeser argumen yang gagal, dan me-reset $OPTINDsetiap percobaan yang gagal. Anda dapat menggunakannya seperti opts "$@"- meskipun Anda ingin menyesuaikan caseloop atau menyimpannya ke dalam variabel dan mengubah bagian itu menjadi eval $case.

opts() while getopts :Rt:f opt || {
             until command shift "$OPTIND" || return 0
                   args=$args' "${'"$((a=${a:-0}+$OPTIND))"'}"'
                   [ -n "${1#"${1#-}"}" ]
             do OPTIND=1; done 2>/dev/null; continue
};     do case "$opt"  in
             R) Rflag=1      ;;
             t) tflag=1      ;
                targ=$OPTARG ;;
             f) fflag=1      ;;
       esac; done

Saat menjalankannya disetel $argske setiap argumen yang getoptstidak menangani ... jadi ...

set -- -R file1 -t /mybackup file2 -f
args= opts "$@"; eval "set -- $args"
printf %s\\n "$args"
printf %s\\n "$@"         
printf %s\\n "$Rflag" "$tflag" "$fflag" "$targ"

KELUARAN

 "${2}" "${5}"
file1
file2
1
1
1
/mybackup

Ini bekerja di bash, dash, zsh, ksh93, mksh... baik, saya berhenti berusaha pada saat itu. Di setiap shell juga punya $[Rtf]flagdan $targ. Intinya adalah bahwa semua angka untuk argumen yang getoptstidak ingin diproses tetap ada.

Mengubah gaya opsi juga tidak membuat perbedaan. Itu bekerja seperti -Rf -t/mybackupatau -R -f -t /mybackup. Itu bekerja di tengah daftar, di akhir daftar, atau di kepala daftar ...

Tetap saja, cara terbaik adalah dengan menempelkan --opsi untuk akhir pada daftar arg Anda dan kemudian lakukan shift "$(($OPTIND-1))"di akhir getoptsproses pemrosesan. Dengan begitu Anda menghapus semua parameter yang diproses dan menjaga ujung daftar argumen.

Satu hal yang saya suka lakukan adalah menerjemahkan opsi panjang menjadi pendek - dan saya melakukannya dengan cara yang sangat mirip karena itulah jawaban ini datang dengan mudah - sebelum saya menjalankan getopts.

i=0
until [ "$((i=$i+1))" -gt "$#" ]
do case "$1"                   in
--Recursive) set -- "$@" "-R"  ;;
--file)      set -- "$@" "-f"  ;;
--target)    set -- "$@" "-t"  ;;
*)           set -- "$@" "$1"  ;;
esac; shift; done
mikeserv
sumber
Konversi opsi panjang Anda juga akan mengonversi non-opsi (sebagai cp -t --file fooatau cp -- --file foo) dan tidak akan mengatasi opsi yang dimasukkan disingkat ( --fi...), atau dengan --target=/destsintaks.
Stéphane Chazelas
@ StéphaneChazelas - hal yang lama mengubah hanya contoh - itu jelas belum dibangun dengan baik. Saya mengganti getoptsbenda itu dengan fungsi yang lebih sederhana.
mikeserv
@ StéphaneChazelas - silakan lihat lagi. Sementara komentar opsi lama tetap dibenarkan, yang pertama - dan downvote yang menyertainya - tidak lagi, seperti yang saya pikirkan. Juga, saya pikir opts()ada kaitannya dengan jawaban Anda sendiri sebagai opts()bekerja dalam satu loop - menyentuh setiap argumen tapi sekali - dan melakukannya dengan andal ( sedekat yang saya tahu) , mudah dibawa, dan tanpa subkulit tunggal.
mikeserv
mengatur ulang OPTIND adalah apa yang saya sebut memulai getopts loop lain. Akibatnya, yang parsing beberapa baris perintah / set pilihan ( -R, -t /mybackup, -f). Saya akan menyimpan suara saya untuk saat ini karena masih dikaburkan, Anda menggunakan yang $abelum diinisialisasi, dan args= opts...kemungkinan argstidak akan disetel (atau disetel seperti sebelumnya) setelah optskembali dalam banyak cangkang (termasuk bash).
Stéphane Chazelas
@ StéphaneChazelas - itu termasuk bash- Saya benci itu. Menurut pendapat saya, jika fungsinya adalah fungsi shell saat ini yang harus dipertahankan. Saya menangani hal-hal ini - karena fungsi saat ini, dengan lebih banyak tes, dapat dibuat untuk menangani semua kasus, dan bahkan untuk menandai argumen dengan opsi sebelumnya, tetapi saya tidak setuju bahwa itu dikaburkan . Seperti yang tertulis, ini adalah cara paling sederhana dan paling langsung untuk menyelesaikan tugasnya yang dapat saya pikirkan. testmaka shifttidak masuk akal ketika dalam setiap shiftkasus gagal Anda harus return. Ini tidak dimaksudkan untuk melakukan sebaliknya.
mikeserv