Fungsi bash yang mengambil argumen seperti bahasa lain?


17

Saya memiliki fungsi bash untuk mengatur $PATHseperti ini -

assign-path()
{
    str=$1
    # if the $PATH is empty, assign it directly.
    if [ -z $PATH ]; then
        PATH=$str;
    # if the $PATH does not contain the substring, append it with ':'.
    elif [[ $PATH != *$str* ]]; then
        PATH=$PATH:$str;
    fi
}

Tetapi masalahnya adalah, saya harus menulis fungsi yang berbeda untuk variabel yang berbeda (misalnya, fungsi lain untuk $CLASSPATHsuka assign-classpath()dll.). Saya tidak dapat menemukan cara untuk meneruskan argumen ke fungsi bash sehingga saya dapat mengaksesnya dengan referensi.

Akan lebih baik jika saya memiliki sesuatu seperti -

assign( bigstr, substr )
{
    if [ -z bigstr ]; then
        bigstr=substr;
    elif [[ bigstr != *str* ]]; then
        bigstr=bigstr:substr;
    fi
}

Ada ide, bagaimana mencapai sesuatu seperti di atas di bash?


Apa "bahasa lain"?
choroba

baik, saya mencoba maksud jika bash memungkinkan "lewat referensi" seperti di c / java dll
ramgorur

1
assign-path /abctidak akan menambahkan /abcke PATH jika $ PATH sudah berisi /abc/def, /abcd, /def/abcdll Terutama Anda tidak dapat menambahkan /binjika PATH sudah berisi /usr/bin.
miracle173

@ miracle173 - ini benar, apa yang perlu Anda lakukan adalah perpecahan $PATHdan uji meniadakan terhadap argumen Anda seperti: add=/bin dir=/usr/bin ; [ -z "${dir%"$add"}" ] || dir="${dir}:${add}". Dalam jawaban saya, saya melakukannya dengan argumen sebanyak yang Anda suka IFS=:.
mikeserv

Terkait dengan masalah spesifik (contoh) tentang menambahkan nilai ke daftar yang dipisahkan dengan titik dua: Bagaimana saya dapat menambahkan dengan bersih  $PATH?  dan Tambahkan direktori $PATHjika belum ada (di Super User ).
Scott

Jawaban:


17

Di dalamnya bashAnda dapat menggunakan ${!varname}untuk memperluas variabel yang dirujuk oleh konten yang lain. Misalnya:

$ var=hello
$ foo () { echo "${!1}"; }
$ foo var
hello

Dari halaman manual:

${!prefix*}
${!prefix@}
       Names matching prefix.  Expands to the names of variables whose names
       begin with prefix, separated by the first character of the IFS special
       variable.  When @ is used  and the expansion appears within double quotes,
       each variable name expands to a separate word.

Juga, untuk mengatur variabel yang dirujuk oleh konten (tanpa bahaya eval), Anda dapat menggunakan declare. Misalnya:

$ var=target
$ declare "$var=hello"
$ echo "$target"
hello

Dengan demikian, Anda dapat menulis fungsi Anda seperti ini (hati-hati karena jika Anda menggunakan declaresuatu fungsi, Anda harus memberi -gatau variabelnya adalah lokal):

shopt -s extglob

assign()
{
  target=$1
  bigstr=${!1}
  substr=$2

  if [ -z "$bigstr" ]; then
    declare -g -- "$target=$substr"
  elif [[ $bigstr != @(|*:)$substr@(|:*) ]]; then
    declare -g -- "$target=$bigstr:$substr"
  fi
}

Dan gunakan seperti:

assign PATH /path/to/binaries

Perhatikan bahwa saya juga telah memperbaiki bug di mana jika substrsudah menjadi substring dari salah satu anggota yang dipisahkan oleh titik dua bigstr, tetapi bukan anggota sendiri, maka itu tidak akan ditambahkan. Misalnya, ini akan memungkinkan penambahan /binke PATHvariabel yang sudah mengandung /usr/bin. Ini menggunakan extglobset untuk mencocokkan awal / akhir string atau titik dua kemudian apa pun. Tanpa extglob, alternatifnya adalah:

[[ $bigstr != $substr && $bigstr != *:$substr &&
   $bigstr != $substr:* && $bigstr != *:$substr:* ]]

-gdi declaretidak tersedia dalam versi bash yang lebih lama, adakah cara agar saya dapat membuat ini kompatibel?
ramgorur

2
@ramgorur, Anda dapat menggunakannya exportuntuk meletakkannya di lingkungan Anda (dengan risiko menimpa sesuatu yang penting) atau eval(berbagai masalah termasuk keamanan jika Anda tidak berhati-hati). Jika menggunakan evalAnda harus baik-baik saja jika Anda suka eval "$target=\$substr". Jika Anda lupa \ , itu akan berpotensi mengeksekusi perintah jika ada ruang dalam konten substr.
Graeme

9

Baru di bash 4.3, adalah -nopsi untuk declare& local:

func() {
    local -n ref="$1"
    ref="hello, world"
}

var='goodbye world'
func var
echo "$var"

Itu tercetak hello, world.


Satu-satunya masalah dengan nameref di Bash adalah bahwa Anda tidak dapat memiliki nameref (dalam fungsi misalnya) yang mereferensikan variabel (di luar fungsi) dengan nama yang sama dengan nameref itu sendiri. Ini mungkin diperbaiki untuk rilis 4.5 sekalipun.
Kusalananda

2

Anda dapat menggunakan evaluntuk mengatur parameter. Deskripsi perintah ini dapat ditemukan sini . Penggunaan berikut evalsalah:

salah(){
  eval $ 1 = $ 2
}

Sehubungan dengan evaluasi tambahan yang evalharus Anda gunakan

menetapkan(){
  eval $ 1 = '$ 2'
}

Periksa hasil menggunakan fungsi-fungsi ini:

$ X1 = '$ X2'
$ X2 = '$ X3'
$ X3 = 'xxx'
$ 
$ echo: $ X1:
: $ X2:
$ echo: $ X2:
: $ X3:
$ echo: $ X3:
: xxx:
$ 
$ salah Y $ X1
$ echo: $ Y:
: $ X3:
$ 
$ assign Y $ X1
$ echo: $ Y:
: $ X2:
$ 
$ assign Y "hallo world"
$ echo: $ Y:
: hallo world:
$ # berikut ini mungkin tidak terduga
$ assign Z $ Y
$ echo ": $ Z:"
: hallo:
$ # jadi Anda harus mengutip argumen kedua jika itu variabel
$ assign Z "$ Y"
$ echo ": $ Z:"
: hallo world:

Tetapi Anda dapat mencapai tujuan Anda tanpa menggunakan eval. Saya lebih suka cara ini yang lebih sederhana.

Fungsi berikut membuat penggantian dengan cara yang benar (saya harap)

menambah(){
  LANCAR lokal = $ 1
  AUGMENT lokal = $ 2
  lokal BARU
  if [[-z $ CURRENT]]; kemudian
    BARU = $ AUGMENT
  Elif [[! (($ CURRENT = $ AUGMENT) || ($ CURRENT = $ AUGMENT: *) || \
    ($ CURRENT = *: $ AUGMENT) || ($ CURRENT = *: $ AUGMENT: *))]]; kemudian
    BARU = $ LANCAR: $ AUGMENT
  lain
    BARU = $ LANCAR
    fi
  gema "$ BARU"
}

Periksa output berikut

augment / usr / bin / bin
/ usr / bin: / bin

augment / usr / bin: / bin / bin
/ usr / bin: / bin

augment / usr / bin: / bin: / usr / local / bin / bin
/ usr / bin: / bin: / usr / local / bin

augment / bin: / usr / bin / bin
/ bin: / usr / bin

augment / bin / bin
/tempat sampah


augment / usr / bin: / bin
/ usr / bin :: / bin

augment / usr / bin: / bin: / bin
/ usr / bin: / bin:

augment / usr / bin: / bin: / usr / local / bin: / bin
/ usr / bin: / bin: / usr / local / bin:

augment / bin: / usr / bin: / bin
/ bin: / usr / bin:

augment / bin: / bin
/tempat sampah:


augment: / bin
::/tempat sampah


menambah "/ usr lib" "/ usr bin"
/ usr lib: / usr bin

menambah "/ usr lib: / usr bin" "/ usr bin"
/ usr lib: / usr bin

Sekarang Anda dapat menggunakan augmentfungsi dengan cara berikut untuk mengatur variabel:

PATH = `augment PATH / bin`
CLASSPATH = `augment CLASSPATH / bin`
LD_LIBRARY_PATH = `menambah LD_LIBRARY_PATH / usr / lib`

Bahkan pernyataan eval Anda salah. Ini, misalnya: v='echo "OHNO!" ; var' ; l=val ; eval $v='$l' - akan menggema "OHNO! Sebelum menugaskan var. Anda dapat" $ {v ## * [; "$ IFS"]} = '$ l' "untuk memastikan bahwa string tidak dapat diperluas ke apa pun yang tidak akan dievaluasi dengan =.
mikeserv

@ mikeserv terima kasih atas komentar Anda, tetapi saya pikir itu bukan contoh yang valid. Argumen pertama dari skrip assign harus berupa nama variabel atau variabel yang berisi nama variabel yang digunakan di sebelah kiri =pernyataan assignment. Anda bisa membantah bahwa skrip saya tidak memeriksa argumennya. Itu benar. Saya bahkan tidak memeriksa apakah ada argumen atau apakah jumlah argumen itu valid. Tapi ini dengan niat. OP dapat menambahkan cek seperti itu jika dia mau.
miracle173

@ mikeserv: Saya pikir proposal Anda untuk mengubah argumen pertama secara diam-diam menjadi nama variabel yang valid bukan ide yang baik: 1) beberapa variabel diatur / ditimpa yang tidak dimaksudkan oleh pengguna. 2) kesalahan disembunyikan dari pengguna fungsi. Itu bukan ide yang bagus. Seseorang seharusnya hanya meningkatkan kesalahan jika itu terjadi.
miracle173

@ mikeserv: Sangat menarik ketika seseorang ingin menggunakan variabel Anda v(lebih baik nilainya) sebagai argumen kedua dari fungsi assign. Jadi nilainya harus di sisi kanan penugasan. Penting untuk mengutip argumen fungsi assign. Saya menambahkan kehalusan ini ke posting saya.
miracle173

Mungkin benar - dan Anda tidak benar-benar menggunakan eval dalam contoh terakhir Anda - yang bijak - jadi tidak masalah. Tapi yang saya katakan adalah bahwa setiap kode yang menggunakan eval dan mengambil input pengguna secara inheren berisiko - dan jika Anda benar-benar menggunakan contoh Anda, saya bisa membuat fungsi yang dirancang untuk mengubah jalur saya ke jalur saya dengan sedikit usaha.
mikeserv

2

Dengan beberapa trik Anda dapat benar-benar mengirimkan parameter bernama ke fungsi, bersama dengan array (diuji dalam bash 3 dan 4).

Metode yang saya kembangkan memungkinkan Anda untuk mengakses parameter yang dikirimkan ke fungsi seperti ini:

testPassingParams() {

    @var hello
    l=4 @array anArrayWithFourElements
    l=2 @array anotherArrayWithTwo
    @var anotherSingle
    @reference table   # references only work in bash >=4.3
    @params anArrayOfVariedSize

    test "$hello" = "$1" && echo correct
    #
    test "${anArrayWithFourElements[0]}" = "$2" && echo correct
    test "${anArrayWithFourElements[1]}" = "$3" && echo correct
    test "${anArrayWithFourElements[2]}" = "$4" && echo correct
    # etc...
    #
    test "${anotherArrayWithTwo[0]}" = "$6" && echo correct
    test "${anotherArrayWithTwo[1]}" = "$7" && echo correct
    #
    test "$anotherSingle" = "$8" && echo correct
    #
    test "${table[test]}" = "works"
    table[inside]="adding a new value"
    #
    # I'm using * just in this example:
    test "${anArrayOfVariedSize[*]}" = "${*:10}" && echo correct
}

fourElements=( a1 a2 "a3 with spaces" a4 )
twoElements=( b1 b2 )
declare -A assocArray
assocArray[test]="works"

testPassingParams "first" "${fourElements[@]}" "${twoElements[@]}" "single with spaces" assocArray "and more... " "even more..."

test "${assocArray[inside]}" = "adding a new value"

Dengan kata lain, tidak hanya Anda dapat memanggil parameter Anda dengan nama mereka (yang merupakan inti yang lebih mudah dibaca), Anda sebenarnya dapat melewatkan array (dan referensi ke variabel - fitur ini hanya bekerja di bash 4.3)! Plus, variabel yang dipetakan semuanya dalam lingkup lokal, seperti $ 1 (dan lainnya).

Kode yang membuat pekerjaan ini cukup ringan dan bekerja baik di bash 3 dan bash 4 (ini adalah satu-satunya versi yang telah saya uji dengan). Jika Anda tertarik pada lebih banyak trik seperti ini yang membuat pengembangan dengan bash jauh lebih baik dan lebih mudah, Anda dapat melihat pada Bash Infinity Framework saya , kode di bawah ini dikembangkan untuk tujuan itu.

Function.AssignParamLocally() {
    local commandWithArgs=( $1 )
    local command="${commandWithArgs[0]}"

    shift

    if [[ "$command" == "trap" || "$command" == "l="* || "$command" == "_type="* ]]
    then
        paramNo+=-1
        return 0
    fi

    if [[ "$command" != "local" ]]
    then
        assignNormalCodeStarted=true
    fi

    local varDeclaration="${commandWithArgs[1]}"
    if [[ $varDeclaration == '-n' ]]
    then
        varDeclaration="${commandWithArgs[2]}"
    fi
    local varName="${varDeclaration%%=*}"

    # var value is only important if making an object later on from it
    local varValue="${varDeclaration#*=}"

    if [[ ! -z $assignVarType ]]
    then
        local previousParamNo=$(expr $paramNo - 1)

        if [[ "$assignVarType" == "array" ]]
        then
            # passing array:
            execute="$assignVarName=( \"\${@:$previousParamNo:$assignArrLength}\" )"
            eval "$execute"
            paramNo+=$(expr $assignArrLength - 1)

            unset assignArrLength
        elif [[ "$assignVarType" == "params" ]]
        then
            execute="$assignVarName=( \"\${@:$previousParamNo}\" )"
            eval "$execute"
        elif [[ "$assignVarType" == "reference" ]]
        then
            execute="$assignVarName=\"\$$previousParamNo\""
            eval "$execute"
        elif [[ ! -z "${!previousParamNo}" ]]
        then
            execute="$assignVarName=\"\$$previousParamNo\""
            eval "$execute"
        fi
    fi

    assignVarType="$__capture_type"
    assignVarName="$varName"
    assignArrLength="$__capture_arrLength"
}

Function.CaptureParams() {
    __capture_type="$_type"
    __capture_arrLength="$l"
}

alias @trapAssign='Function.CaptureParams; trap "declare -i \"paramNo+=1\"; Function.AssignParamLocally \"\$BASH_COMMAND\" \"\$@\"; [[ \$assignNormalCodeStarted = true ]] && trap - DEBUG && unset assignVarType && unset assignVarName && unset assignNormalCodeStarted && unset paramNo" DEBUG; '
alias @param='@trapAssign local'
alias @reference='_type=reference @trapAssign local -n'
alias @var='_type=var @param'
alias @params='_type=params @param'
alias @array='_type=array @param'

1
assign () 
{ 
    if [ -z ${!1} ]; then
        eval $1=$2
    else
        if [[ ${!1} != *$2* ]]; then
            eval $1=${!1}:$2
        fi
    fi
}

$ echo =$x=
==
$ assign x y
$ echo =$x=
=y=
$ assign x y
$ echo =$x=
=y=
$ assign x z
$ echo =$x=
=y:z=

Apakah ini cocok?


hai, saya mencoba melakukannya seperti milik Anda, tetapi tidak berhasil, maaf sudah menjadi pemula. Bisakah Anda memberi tahu saya apa yang salah dengan skrip ini ?
ramgorur

1
Penggunaan Anda evalrentan terhadap eksekusi perintah sewenang-wenang.
Chris Down

Anda punya ide untuk membuat evalsaluran lebih aman? Biasanya, ketika saya pikir saya perlu eval, saya memutuskan untuk tidak menggunakan * sh dan beralih ke bahasa lain. Di sisi lain, menggunakan ini dalam skrip untuk menambahkan entri ke beberapa variabel seperti PATH, itu akan berjalan dengan konstanta string dan bukan input pengguna ...

1
Anda dapat melakukannya evaldengan aman - tetapi butuh banyak pemikiran. Jika Anda hanya mencoba mereferensikan suatu parameter, Anda ingin melakukan sesuatu seperti ini: dengan eval "$1=\"\$2\""cara itu pada eval'spass pertama, ia hanya mengevaluasi $ 1 dan pada detik itu bernilai = "$ 2". Tetapi Anda perlu melakukan sesuatu yang lain - ini tidak perlu di sini.
mikeserv

Sebenarnya, komentar saya di atas juga salah. Anda harus melakukannya "${1##*[;"$IFS"]}=\"\$2\""- dan bahkan itu tanpa jaminan. Atau eval "$(set -- $1 ; shift $(($#-1)) ; echo $1)=\"\$2\"". Ini tidak mudah.
mikeserv

1

Argumen yang dinamai bukan bagaimana sintaks Bash dirancang. Bash dirancang untuk menjadi perbaikan berulang pada kulit Bourne. Karena itu perlu memastikan hal-hal tertentu bekerja antara dua cangkang sebanyak mungkin. Jadi itu tidak dimaksudkan untuk menjadi lebih mudah untuk skrip dengan keseluruhan, itu hanya dimaksudkan untuk menjadi lebih baik daripada Bourne sambil memastikan bahwa jika Anda mengambil skrip dari lingkungan Bourne ke bashsemudah mungkin. Itu tidak sepele karena banyak cangkang masih memperlakukan Bourne sebagai standar de facto. Karena orang menulis skrip mereka agar kompatibel dengan Bourne (untuk portabilitas ini) kebutuhan tetap berlaku dan tidak mungkin untuk pernah berubah.

Anda mungkin lebih baik melihat skrip shell yang berbeda (seperti pythonatau sesuatu) sepenuhnya jika itu layak. Jika Anda menghadapi keterbatasan bahasa, Anda harus mulai menggunakan bahasa baru.


Mungkin di awal bashkehidupan ini benar. Tetapi sekarang ketentuan khusus dibuat. Variabel referensi lengkap sekarang tersedia di bash 4.3- lihat jawaban derobert .
Graeme

Dan jika Anda melihat di sini, Anda akan melihat bahwa Anda dapat melakukan hal ini dengan sangat mudah bahkan hanya dengan kode portabel POSIX: unix.stackexchange.com/a/120531/52934
mikeserv

1

Dengan shsintaks standar (akan berfungsi bash, dan tidak hanya di bash), Anda dapat melakukan:

assign() {
  eval '
    case :${'"$1"'}: in
      (::) '"$1"'=$2;;   # was empty, copy
      (*:"$2":*) ;;      # already there, do nothing
      (*) '"$1"'=$1:$2;; # otherwise, append with a :
    esac'
}

Seperti untuk solusi menggunakan bash's declare, itu aman selama $1mengandung nama variabel yang valid.


0

TENTANG ARGS NAMED:

Ini sangat sederhana dilakukan dan bashtidak diperlukan sama sekali - ini adalah perilaku penugasan POSIX dasar yang ditentukan melalui ekspansi parameter:

: ${PATH:=this is only assigned to \$PATH if \$PATH is null or unset}

Untuk melakukan demo dengan nada yang mirip dengan @Graeme, tetapi dengan cara yang portabel:

_fn() { echo "$1 ${2:-"$1"} $str" ; }

% str= ; _fn "${str:=hello}"
> hello hello hello

Dan di sana saya hanya melakukan str=untuk memastikan ia memiliki nilai nol, karena ekspansi parameter memiliki perlindungan bawaan terhadap penetapan ulang lingkungan shell inline jika sudah ditetapkan.

LARUTAN:

Untuk masalah spesifik Anda, saya tidak percaya bahwa argumen bernama diperlukan, meskipun tentu saja mungkin. Gunakan $IFSsebaliknya:

assign() { oFS=$IFS ; IFS=: ; add=$* 
    set -- $PATH ; for p in $add ; do { 
        for d ; do [ -z "${d%"$p"}" ] && break 
        done ; } || set -- $* $p ; done
    PATH= ; echo "${PATH:="$*"}" ; IFS=$oFS
}

Inilah yang saya dapatkan ketika saya menjalankannya:

% PATH=/usr/bin:/usr/yes/bin
% assign \
    /usr/bin \
    /usr/yes/bin \
    /usr/nope/bin \
    /usr/bin \
    /nope/usr/bin \
    /usr/nope/bin

> /usr/bin:/usr/yes/bin:/usr/nope/bin:/nope/usr/bin

% echo "$PATH"
> /usr/bin:/usr/yes/bin:/usr/nope/bin:/nope/usr/bin

% dir="/some crazy/dir"
% p=`assign /usr/bin /usr/bin/new "$dir"`
% echo "$p" ; echo "$PATH"
> /usr/bin:/usr/yes/bin:/usr/nope/bin:/nope/usr/bin:/some crazy/dir:/usr/bin/new
> /usr/bin:/usr/yes/bin:/usr/nope/bin:/nope/usr/bin:/some crazy/dir:/usr/bin/new

Perhatikan itu hanya menambahkan argumen yang belum ada $PATHatau yang datang sebelumnya? Atau bahkan butuh lebih dari satu argumen? $IFSberguna.


hai, saya gagal mengikuti, jadi bisakah Anda menjelaskannya sedikit lebih banyak? Terima kasih.
ramgorur

Saya sudah melakukannya ... Tolong, beberapa saat lagi ...
mikeserv

@ramgorur Ada yang lebih baik? Maaf, tetapi kehidupan nyata mengganggu dan butuh saya sedikit lebih lama dari yang saya harapkan untuk menyelesaikan penulisan.
mikeserv

sama di sini juga, menyerah pada kehidupan nyata. Sepertinya banyak pendekatan berbeda untuk mengkodekan hal ini, izinkan saya memberi saya waktu untuk menyelesaikan yang terbaik.
ramgorur

@ramgorur - tentu saja, hanya ingin memastikan saya tidak meninggalkan Anda menggantung. Tentang ini - Anda memilih apa pun yang Anda mau, teman. Saya akan mengatakan tidak ada jawaban lain yang bisa saya lihat menawarkan sebagai solusi yang ringkas, portabel, atau sekuat yang ada di assignsini. Jika Anda memiliki pertanyaan tentang cara kerjanya, saya akan dengan senang hati menjawab. Omong-omong, jika Anda benar-benar ingin menamai argumen, Anda mungkin ingin melihat jawaban saya yang lain ini di mana saya menunjukkan cara mendeklarasikan fungsi yang dinamai argumen fungsi lain: unix.stackexchange.com/a/120531/52934
mikeserv

-2

Saya tidak dapat menemukan apa pun seperti ruby, python, dll. Tetapi ini terasa lebih dekat dengan saya

foo() {
  BAR="$1"; BAZ="$2"; QUUX="$3"; CORGE="$4"
  ...
}

Keterbacaan menurut saya lebih baik, 4 baris berlebihan untuk menyatakan nama parameter Anda. Terlihat lebih dekat dengan bahasa modern juga.


(1) Pertanyaannya adalah tentang fungsi shell. Jawaban Anda menyajikan fungsi kerangka kerangka. Selain itu, jawaban Anda tidak ada hubungannya dengan pertanyaan. (2) Anda pikir itu meningkatkan keterbacaan untuk mengambil garis yang terpisah dan menyatukannya menjadi satu baris, dengan titik koma sebagai pemisah? Saya percaya bahwa Anda mendapatkannya mundur; bahwa gaya satu garis Anda  kurang dapat dibaca daripada gaya multi-garis.
Scott

Jika mengurangi bagian yang tidak perlu adalah intinya, lalu mengapa tanda kutip dan titik koma? a=$1 b=$2 ...bekerja dengan baik.
ilkkachu
Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.