Cara mendefinisikan fungsi bash serupa sekaligus

10

Saya memiliki fungsi-fungsi ini di ~/.bashrc:

function guard() {
    if [ -e 'Gemfile' ]; then
    bundle exec guard "$@"
    else
    command guard "$@"
    fi
}
function rspec() {
    if [ -e 'Gemfile' ]; then
    bundle exec rspec "$@"
    else
    command rspec "$@"
    fi
}
function rake() {
    if [ -e 'Gemfile' ]; then
        bundle exec rake "$@"
    else
        command rake "$@"
    fi
}

Seperti yang Anda lihat, fungsi-fungsi ini sangat mirip. Saya ingin mendefinisikan 3 fungsi ini sekaligus. Apakah ada cara untuk membuatnya?

lingkungan Hidup

bash --version
GNU bash, version 3.2.51(1)-release (x86_64-apple-darwin13)
pasir besi
sumber

Jawaban:

8
$ cat t.sh
#!/bin/bash

for func in guard rspec rake; do
        eval "
        ${func}() {
                local foo=(command ${func})
                [ -e 'Gemfile' ] && foo=(bundle exec ${func})
                \"\${foo[@]}\" \"\$@\"
        }
        "
done

type guard rspec rake

.

$ ./t.sh
guard is a function
guard ()
{
    local foo=(command guard);
    [ -e 'Gemfile' ] && foo=(bundle exec guard);
    "${foo[@]}" "$@"
}
rspec is a function
rspec ()
{
    local foo=(command rspec);
    [ -e 'Gemfile' ] && foo=(bundle exec rspec);
    "${foo[@]}" "$@"
}
rake is a function
rake ()
{
    local foo=(command rake);
    [ -e 'Gemfile' ] && foo=(bundle exec rake);
    "${foo[@]}" "$@"
}

Peringatan biasa tentang evalberlaku.

Adrian Frühwirth
sumber
Apakah itu tidak dimakan oleh for loop?maksud saya, variabel yang dideklarasikan secara for loopumum menghilang - saya harapkan fungsi yang sama untuk alasan yang sama.
mikeserv
Apa yang membuatmu berpikir demikian? bash -c 'for i in 1; do :; done; echo $i'=> 1. The typejelas menunjukkan bahwa fungsi ada di luar lingkup loop.
Adrian Frühwirth
1
@mikeserv. Bahkan dengan bashpelingkupan dinamis yang bisa Anda dapatkan adalah localvariabel lokal untuk ruang lingkup seluruh fungsi , variabel pasti tidak "menghilang" setelah satu loop. Bahkan, karena tidak ada fungsi yang terlibat di sini, bahkan tidak mungkin untuk mendefinisikan localvariabel dalam kasus ini.
Adrian Frühwirth
Kanan - lokal ke for loop - mereka dicakup secara lokal. Mereka menghilang segera setelah shell lingkaran orangtua mereka. Apakah ini tidak terjadi di sini?
mikeserv
Tidak, seperti yang saya jelaskan tidak ada konsep seperti "local to the for loop" dalam skrip shell dan postingan saya dan contoh dalam komentar saya di atas dengan jelas menunjukkan hal itu.
Adrian Frühwirth
7
_gem_dec() { shift $# ; . /dev/fd/3
} 3<<-FUNC
    _${1}() { [ ! -e 'Gemfile' ] && { 
        command $1 "\$@" ; return \$?
        } || bundle exec $1 "\$@"
    }
FUNC
for func in guard rspec rake ; do _gem_dec $func ; done
echo "_guard ; _rspec ; _rake are all functions now."

Surat wasiat di atas . source /dev/fd/3yang dimasukkan ke dalam _gem_dec()fungsi setiap kali disebut sebagai satu- here-document. _gem_dec'ssatunya pekerjaan yang dievaluasi adalah untuk menerima satu parameter dan pra-evaluasi itu sebagai bundle exectarget dan sebagai nama fungsi di mana ia ditargetkan.

NOTE: . sourcing shell expansions results in twice-evaluated variables - just like eval. It can be risky.

Namun dalam kasus di atas, saya pikir tidak ada risiko.

Jika blok-kode di atas disalin ke .bashrcfile, shell tidak hanya akan berfungsi _guard(), _rspec()dan_rake() dideklarasikan saat login, tetapi _gem_dec()fungsi tersebut juga akan tersedia untuk dieksekusi kapan saja di prompt shell Anda (atau sebaliknya) sehingga fungsi templated baru dapat dideklarasikan kapan pun Anda suka hanya dengan:

_gem_dec $new_templated_function_name

Dan terima kasih kepada @Andrew karena menunjukkan kepada saya bahwa ini tidak akan dimakan oleh for loop.

TAPI BAGAIMANA CARANYA?

Saya menggunakan 3deskriptor file di atas untuk tetap stdin, stdout, and stderr, or <&0 >&1 >&2terbuka dari kebiasaan - meskipun, seperti juga kasus untuk beberapa tindakan pencegahan standar lainnya yang saya terapkan di sini - karena fungsi yang dihasilkan sangat sederhana, itu benar-benar tidak perlu. Ini adalah praktik yang baik. Memanggil shift $#adalah tindakan pencegahan lain yang tidak perlu.

Namun, ketika file ditentukan sebagai <inputatau>output dengan [optional num]<fileatau [optional num]>filepengalihan kernel membacanya menjadi deskriptor file, yang dapat diakses melalui character devicefile khusus di /dev/fd/[0-9]*. Jika [optional num]specifier dihilangkan, maka 0<filediasumsikan untuk input dan 1>fileuntuk output. Pertimbangkan ini:

l='line %d\n' ; printf "$l" 1 2 3 4 5 6 >/dev/fd/1
> line 1
> line 2
> line 3
> line 4
> line 5
> line 6

( printf "$l" 4 5 6 >/dev/fd/3 ; printf "$l" 1 2 3 ) >/tmp/sample 3>/tmp/sample2

( cat /tmp/sample2 ) </tmp/sample
> line 4
> line 5
> line 6

( cat /dev/fd/0 ) </tmp/sample
> line 1
> line 2
> line 3

( cat /dev/fd/3 ) </tmp/sample 3</tmp/sample2
> line 4
> line 5
> line 6

Dan karena a here-documenthanya sarana untuk menggambarkan file inline dalam blok-kode, ketika kita melakukannya:

<<'HEREDOC'
[$CODE]
HEREDOC

Kita mungkin juga melakukan:

echo '[$CODE]' >/dev/fd/0

Dengan satu perbedaan yang sangat penting . Jika Anda tidak "'\quote'"yang <<"'\LIMITER"'dari here-documentmaka shell akan mengevaluasi untuk shell $expansionseperti:

echo "[$CODE]" >/dev/fd/0

Jadi, untuk _gem_dec(), yang 3<<-FUNC here-documentdievaluasi sebagai file input, sama seperti itu akan jika itu 3<~/some.file kecuali bahwa karena kita meninggalkan FUNClimiter bebas dari kutipan, itu pertama dievaluasi untuk $expansion.Yang penting tentang ini adalah bahwa itu adalah input, artinya itu hanya ada untuk _gem_dec(),tetapi itu juga dievaluasi sebelum _gem_dec()fungsi berjalan karena shell kita harus membaca dan mengevaluasi $expansionssebelum menyerahkannya sebagai input.

Mari kita lakukan guard,misalnya:

_gem_dec guard

Jadi pertama-tama shell harus menangani input, yang berarti membaca:

3<<-FUNC
    _${1}() { [ ! -e 'Gemfile' ] && { 
        command $1 "\$@" ; return \$?
        } || bundle exec $1 "\$@"
    }
FUNC

Ke deskriptor file 3 dan mengevaluasinya untuk ekspansi shell. Jika saat ini Anda berlari:

cat /dev/fd/3

Atau:

cat <&3

Karena keduanya adalah perintah yang setara, Anda akan melihat *:

_guard() { [ ! -e 'Gemfile' ] && { 
    command guard "$@" ; return $?
    } || bundle exec guard "$@"
}

... sebelum kode apa pun dalam fungsi dijalankan sama sekali. Ini adalah fungsi ini <input, setelah semua. Untuk lebih banyak contoh, lihat jawaban saya untuk pertanyaan berbeda di sini .

(* Secara teknis ini tidak sepenuhnya benar. Karena saya menggunakan yang memimpin -dashsebelum here-doc limiter, semua di atas akan dibenarkan. Tapi saya menggunakan yang -dashsaya bisa <tab-insert>untuk keterbacaan di tempat pertama sehingga saya tidak akan mengupas <tab-inserts>sebelumnya) menawarkannya kepada Anda untuk dibaca ...)

Bagian terbaik tentang ini adalah kutipan - pemberitahuan bahwa '"tanda kutip tetap dan hanya \tanda kutip yang dilucuti. Mungkin karena alasan ini lebih dari yang lain bahwa jika Anda harus mengevaluasi shell dua kali, $expansionsaya akan merekomendasikan here-documentkarena penawarannya jauh lebih mudah daripada eval.

Lagi pula, sekarang kode di atas persis seperti file yang dimasukkan seperti 3<~/heredoc.filehanya menunggu _gem_dec()fungsi untuk pergi dan menerima inputnya /dev/fd/3.

Jadi ketika kita memulai _gem_dec()hal pertama yang saya lakukan adalah melemparkan semua parameter posisi, karena langkah kita selanjutnya adalah ekspansi shell yang dievaluasi dua kali dan saya tidak ingin semua yang ada $expansionsditafsirkan sebagai salah satu $1 $2 $3...parameter saya saat ini . Jadi saya:

shift $#

shiftbuang sebanyak yang positional parametersAnda tentukan dan mulai dari $1yang tersisa. Jadi jika saya menelepon _gem_dec one two threepada prompt _gem_dec's $1 $2 $3parameter posisi akan one two threedan jumlah total posisi saat ini, atau $#akan 3. Jika saya kemudian disebut shift 2,nilai-nilai onedantwo akan shifted jauh, nilai $1akan berubah menjadi threedan $#akan memperluas untuk 1. Jadi shift $#hanya membuang mereka semua. Melakukan ini benar-benar pencegahan dan hanya kebiasaan yang telah saya kembangkan setelah melakukan hal semacam ini untuk sementara waktu. Di sini (subshell)tersebar sedikit demi kejelasan:

( set -- one two three ; echo "$1 $2 $3" ; echo $# )
> one two three
> 3

( set -- one two three ; shift 2 ; echo "$1 $2 $3" ; echo $# )
> three
> 1

( set -- one two three ; shift $# ; echo "$1 $2 $3" ; echo $# )
>
> 0

Bagaimanapun, langkah selanjutnya adalah di mana keajaiban terjadi. Jika Anda . ~/some.shdi prompt shell maka semua fungsi dan variabel lingkungan yang dideklarasikan di ~/some.shkemudian akan dipanggil di prompt shell Anda. Hal yang sama berlaku di sini, kecuali kita . source yang character devicefile khusus untuk file descriptor kami, atau . /dev/fd/3- yang mana kami here-documentfile dalam-line telah pathed - dan kami telah menyatakan fungsi kita. Dan begitulah cara kerjanya.

_guard

Sekarang lakukan apa pun _guardyang seharusnya dilakukan fungsi Anda .

Tambahan:

Cara yang bagus untuk mengatakan simpan posisi Anda:

f() { . /dev/fd/3
} 3<<-ARGS
    args='${args:-"$@"}'
ARGS

EDIT:

Ketika saya pertama kali menjawab pertanyaan ini, saya lebih fokus pada masalah mendeklarasikan sebuah shell yang function()mampu mendeklarasikan fungsi-fungsi lain yang akan tetap ada di $ENViron shell saat ini daripada yang saya lakukan pada apa yang penanya akan lakukan dengan fungsi-fungsi persisten tersebut. Sejak itu saya menyadari bahwa solusi yang saya tawarkan pada awalnya 3<<-FUNCberupa:

3<<-FUNC
    _${1}() { 
        if [ -e 'Gemfile' ]; then
            bundle exec $1 "\$@"
        else 
            command _${1} "\$@"
    }
FUNC

Akan kemungkinan besar tidak bekerja seperti yang diharapkan untuk penanya karena saya secara khusus mengubah nama fungsi deklaratif ini dari $1ke _${1}mana, jika disebut seperti _gem_dec guardmisalnya, akan menghasilkan _gem_decmendeklarasikan fungsi bernama _guardsebagai lawan hanya guard.

Catatan: Perilaku seperti itu merupakan kebiasaan bagi saya - saya biasanya beroperasi dengan anggapan bahwa fungsi shell harus menempati hanya milik mereka sendiri_namespaceuntuk menghindari intrusi mereka padanamespaceshell yangcommandstepat.

Namun, ini bukan kebiasaan universal, sebagaimana diperlihatkan dalam penggunaan penanya commanduntuk dipanggil $1.

Pemeriksaan lebih lanjut membuat saya percaya hal-hal berikut:

Penanya ingin fungsi shell dinamai guard, rspec, or rake, ketika dipanggil, akan mengkompilasi rubyfungsi yang sama dengan nama iffile yang Gemfileada di $PATH OR if Gemfile tidak ada, fungsi shell harus menjalankan rubyfungsi dengan nama yang sama.

Ini tidak akan berhasil sebelumnya karena saya juga mengubah yang $1dipanggil oleh commanduntuk membaca:

command _${1}

Yang tidak akan menghasilkan eksekusi rubyfungsi yang dikompilasi oleh fungsi shell sebagai:

bundle exec $1

Saya harap Anda dapat melihat (seperti yang akhirnya saya lakukan) bahwa tampaknya penanya hanya menggunakan commandsama sekali untuk menentukan secara tidak langsung namespacekarena commandakan lebih suka memanggil file yang dapat dieksekusi di $PATHatas fungsi shell dengan nama yang sama.

Jika analisis saya benar (karena saya harap penanya akan mengonfirmasi) maka ini:

_${1}() { [ ! -e 'Gemfile' ] && { 
    command $1 "\$@" ; return \$?
    } || bundle exec $1 "\$@"
}

Seharusnya lebih baik memenuhi persyaratan tersebut dengan pengecualian bahwa panggilan guardpada prompt hanya akan mencoba untuk mengeksekusi file yang dapat dieksekusi dalam $PATHnama guardsedangkan panggilan _guardpada prompt akan memeriksa Gemfile'skeberadaan dan mengkompilasi sesuai atau mengeksekusi guarddieksekusi di $PATH. Dengan cara namespaceini dilindungi dan, setidaknya saat saya melihatnya, niat si penanya masih terpenuhi.

Bahkan, menganggap fungsi shell kami _${1}()dan dieksekusi ${PATH}/${1}adalah hanya dua cara shell kami bisa menafsirkan panggilan baik $1atau _${1}maka penggunaan commandfungsi sama sekali sekarang seluruhnya terbuat berlebihan. Namun, saya membiarkannya tetap karena saya tidak suka melakukan kesalahan yang sama dua kali ... berturut-turut.

Jika ini tidak dapat diterima oleh si penanya dan dia lebih suka untuk menghilangkan _sama sekali maka, dalam bentuk saat ini, mengedit _underscorekeluar harus semua penanya perlu lakukan untuk memenuhi persyaratannya seperti yang saya mengerti.

Selain perubahan yang saya juga telah mengedit fungsi untuk menggunakan &&dan / atau|| shell arus pendek conditional daripada aslinya if/thensintaks. Dengan cara ini commandpernyataan hanya dievaluasi sama sekali jika Gemfiletidak ada $PATH. Modifikasi ini memang membutuhkan penambahan return $?namun untuk memastikan bundlepernyataan tidak berjalan jika Gemfiletidak ada tetapi ruby $1fungsi mengembalikan apa pun selain 0.

Terakhir, saya harus perhatikan bahwa solusi ini hanya mengimplementasikan konstruksi shell portabel. Dengan kata lain, ini harus menghasilkan hasil yang identik dalam setiap shell yang mengklaim kompatibilitas POSIX. Walaupun tentu saja akan menjadi omong kosong bagi saya untuk mengklaim setiap sistem yang kompatibel dengan POSIX harus menangani ruby bundlearahan, setidaknya perintah shell yang memanggilnya harus berperilaku sama terlepas dari apakah shell panggilannya shatau tidak dash. Juga hal di atas akan bekerja seperti yang diharapkan (anggap setidaknya setengah waras shopts) di keduanya bashdan zsh.

mikeserv
sumber
Saya memasukkan kode Anda ~/.bashrcdan menelepon . ~/.bashrc, lalu ketiga fungsi ini dijalankan. Mungkin perilakunya berbeda dengan lingkungan, jadi saya menambahkan lingkungan saya ke pertanyaan. Selain itu saya tidak mengerti mengapa baris terakhir _guard ; _rspec ; _rakediperlukan. Saya mencari shiftdan mengajukan deskriptor, sepertinya ini di luar pemahaman saya saat ini.
ironsand
Saya hanya meletakkannya di sana untuk menunjukkan bahwa mereka dapat dipanggil. Maaf - Saya memasukkan gema sekarang. Jadi, Anda dapat memanggil mereka sebagai fungsi, seperti yang telah Anda tunjukkan.
mikeserv
@Tetsu - apakah lebih masuk akal sekarang?
mikeserv
Saya membaca jawaban Anda 3 kali, tetapi dengan jujur ​​mengatakan saya perlu lebih banyak pengetahuan untuk memahami penjelasannya. Meskipun saya sangat berterima kasih kepada Anda, saya akan membacanya lagi ketika saya mendapatkan lebih banyak pengalaman.
ironsand
@Tetsu Mungkin lebih jelas sekarang ...? Saya pikir saya menyadari, dan sekarang telah memperbaiki, kesalahan yang saya buat sebelumnya. Tolong beri tahu saya, jika Anda mau.
mikeserv
2
function threeinone () {
    local var="$1"
    if [ $# -ne 1 ]; then
        return 1
    fi
    if ! [ "$1" = "guard" -o "$1" = "rspec" -o "$1" = "rake" ]; then
        return 1
    fi
    shift
    if [ -e 'Gemfile' ]; then
        bundle exec "$var" "$@"
    else
        command "$var" "$@"
    fi
}

threeinone guard
threeinone rspec
threeinone rake
Hauke ​​Laging
sumber