Funkcja bash, która bierze argument jak inne języki?

17

Mam funkcję bash, aby ustawić w $PATHten sposób -

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
}

Ale problem polega na tym, że muszę napisać inną funkcję dla różnych zmiennych (na przykład inną funkcję dla $CLASSPATHpodobnych assign-classpath()itp.). Nie mogłem znaleźć sposobu na przekazanie argumentu do funkcji bash, aby móc uzyskać do niej dostęp przez odniesienie.

Byłoby lepiej, gdybym miał coś takiego -

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

Każdy pomysł, jak osiągnąć coś takiego jak wyżej w bash?

ramgorur
źródło
Jakie „inne języki”?
choroba
cóż, starałem się powiedzieć, czy bash pozwala na „przejście przez referencję” jak w c / java itp.
ramgorur
1
assign-path /abcnie dołączy /abcdo PATH jeśli $ PATH zawiera już /abc/def, /abcd, /def/abcitd. Szczególnie nie można dodać /bin, jeśli PATH zawiera już /usr/bin.
miracle173
@ miracle173 - to prawda, co trzeba zrobić, to rozłam $PATHi anulować sprawdzian przed swoimi argumentami jak: add=/bin dir=/usr/bin ; [ -z "${dir%"$add"}" ] || dir="${dir}:${add}". W mojej odpowiedzi robię to w ten sposób, używając tylu argumentów, ile tylko chcesz IFS=:.
mikeserv
Powiązane z konkretnym (wystąpieniem) problemem dodawania wartości do list oddzielonych dwukropkiem: Jak mogę czysto dodać  $PATH?  i Dodaj katalog do, $PATHjeśli jeszcze go nie ma (w trybie Super User ).
Scott

Odpowiedzi:

17

W bashmożesz użyć ${!varname}do rozwinięcia zmiennej, do której odwołuje się zawartość innej. Na przykład:

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

Ze strony podręcznika:

${!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.

Ponadto, aby ustawić zmienną, do której odwołuje się zawartość (bez niebezpieczeństw eval), możesz użyć declare. Na przykład:

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

Tak więc możesz napisać swoją funkcję w ten sposób (uważaj, ponieważ jeśli używasz declarew funkcji, musisz dać -glub zmienna będzie lokalna):

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
}

I używaj go w następujący sposób:

assign PATH /path/to/binaries

Zauważ, że poprawiłem również błąd, w którym jeśli substrjest już podciągiem jednego z członków oddzielonych dwukropkami bigstr, ale nie jest jego własnym członkiem, wówczas nie zostanie dodany. Na przykład pozwoliłoby to na dodanie /bindo PATHzmiennej już zawierającej /usr/bin. Używa extglobzestawów, aby dopasować początek / koniec łańcucha lub dwukropek, a następnie cokolwiek innego. Bez extglobtego alternatywą byłoby:

[[ $bigstr != $substr && $bigstr != *:$substr &&
   $bigstr != $substr:* && $bigstr != *:$substr:* ]]
Graeme
źródło
-gin declarenie jest dostępny w starszej wersji bash, czy jest jakiś sposób, aby uczynić to wstecznym kompatybilnym?
ramgorur
2
@ramgorur, możesz użyć, exportaby umieścić go w swoim środowisku (na ryzyko zastąpienia czegoś ważnego) lub eval(różne problemy, w tym bezpieczeństwo, jeśli nie jesteś ostrożny). Jeśli używasz eval, powinieneś być w porządku, jeśli robisz to tak eval "$target=\$substr". Jeśli jednak zapomnisz \ , potencjalnie wykona polecenie, jeśli w treści jest spacja substr.
Graeme
9

Nowy w bash 4.3, jest -nopcja declare& local:

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

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

To się drukuje hello, world.

derobert
źródło
Jedynym problemem związanym z nazwami w Bash jest to, że nie można mieć nazwy (na przykład w funkcji) odwołującej się do zmiennej (poza funkcją) o tej samej nazwie, co sama nazwa. Można to jednak naprawić w wersji 4.5.
Kusalananda
2

Możesz użyć evaldo ustawienia parametru. Opis tego polecenia można znaleźć tutaj . Następujące użycie evaljest nieprawidłowe:

źle(){
  eval 1 $ = 2 $
}

W odniesieniu do dodatkowej oceny evalnależy skorzystać

przydzielać(){
  eval 1 $ = „2 $”
}

Sprawdź wyniki korzystania z tych funkcji:

$ X1 = „$ X2”
$ X2 = „$ X3”
X3 USD = „xxx”
$ 
$ echo: $ X1:
: $ X2:
$ echo: $ X2:
: $ X3:
$ echo: $ X3:
: xxx:
$ 
$ źle Y $ X1
$ echo: $ Y:
: $ X3:
$ 
przypisz Y Y X1
$ echo: $ Y:
: $ X2:
$ 
przypisz Y „hallo world”
$ echo: $ Y:
: witaj świecie:
$ # następujące mogą być nieoczekiwane
przypisz Z $ Y
$ echo ": $ Z:"
:halo:
$ #, więc musisz podać drugi argument, jeśli jest to zmienna
$ przypisać Z „$ Y”
$ echo ": $ Z:"
: witaj świecie:

Ale możesz osiągnąć swój cel bez użycia eval. Wolę ten sposób, który jest prostszy.

Poniższa funkcja sprawia, że ​​podstawienie we właściwy sposób (mam nadzieję)

zwiększać(){
  lokalny BIEŻĄCY = 1 $
  lokalny AUGMENT = 2 USD
  lokalny NOWOŚĆ
  jeśli [[-z $ CURRENT]]; następnie
    NOWOŚĆ = AUGMENT $
  elif [[! (($ CURRENT = $ AUGMENT) || ($ CURRENT = $ AUGMENT: *) || \
    ($ CURRENT = *: $ AUGMENT) || ($ CURRENT = *: $ AUGMENT: *))]]; następnie
    NOWOŚĆ = $ CURRENT: $ AUGMENT
  jeszcze
    NOWOŚĆ = $ CURRENT
    fi
  echo „$ NOWOŚĆ”
}

Sprawdź następujące dane wyjściowe

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
/kosz


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
/kosz:


augment: / bin
::/kosz


augment "/ usr lib" "/ usr bin"
/ usr lib: / usr bin

augment "/ usr lib: / usr bin" "/ usr bin"
/ usr lib: / usr bin

Teraz możesz użyć augmentfunkcji w następujący sposób, aby ustawić zmienną:

ŚCIEŻKA = `aATMENT PATH / bin`
CLASSPATH = `augment CLASSPATH / bin`
LD_LIBRARY_PATH = `augment LD_LIBRARY_PATH / usr / lib`
cud173
źródło
Nawet twoje stwierdzenie ewaluacji jest błędne. To na przykład: v='echo "OHNO!" ; var' ; l=val ; eval $v='$l' - echo „OHNO!” Przed przypisaniem var. Możesz „$ {v ## * [;„ $ IFS ”]}} =„ $ l ””, aby upewnić się, że łańcuch nie może rozwinąć się do niczego, co nie będzie oceniane za pomocą =.
mikeserv
@mikeserv dziękuję za komentarz, ale uważam, że nie jest to prawidłowy przykład. Pierwszym argumentem skryptu przypisania powinna być nazwa zmiennej lub zmienna zawierająca nazwę zmiennej, która jest używana po lewej stronie instrukcji =przypisania. Możesz argumentować, że mój skrypt nie sprawdza swojego argumentu. To prawda. Nawet nie sprawdzam, czy jest jakiś argument lub czy liczba argumentów jest poprawna. Ale było to zamierzone. OP może dodać takie kontrole, jeśli chce.
miracle173
@mikeserv: Myślę, że twoja propozycja cichego przekształcenia pierwszego argumentu na prawidłową nazwę zmiennej nie jest dobrym pomysłem: 1) niektóre zmienne są ustawione / nadpisane, co nie było zamierzone przez użytkownika. 2) błąd jest ukryty przed użytkownikiem funkcji. To nigdy nie jest dobry pomysł. W takim przypadku należy po prostu zgłosić błąd.
miracle173
@mikeserv: Interesujące jest, gdy chce się użyć zmiennej v(lepiej jej wartości) jako drugiego argumentu funkcji przypisania. Dlatego jego wartość powinna znajdować się po prawej stronie zadania. Konieczne jest zacytowanie argumentu funkcji assign. Dodałem tę subtelność do mojego posta.
miracle173
Prawdopodobnie prawda - i tak naprawdę nie używasz eval w swoim ostatnim przykładzie - co jest mądre - więc to tak naprawdę nie ma znaczenia. Ale mówię o tym, że każdy kod, który używa eval i przyjmuje dane wejściowe od użytkownika, jest z natury ryzykowny - a jeśli użyjesz swojego przykładu, mógłbym zaprojektować funkcję, aby zmienić moją ścieżkę na rm moją ścieżkę przy niewielkim wysiłku.
mikeserv
2

Za pomocą kilku sztuczek możesz przekazać nazwane parametry do funkcji wraz z tablicami (testowane w bash 3 i 4).

Opracowana przeze mnie metoda umożliwia dostęp do parametrów przekazywanych do funkcji takiej jak ta:

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"

Innymi słowy, nie tylko możesz wywoływać swoje parametry według ich nazw (co stanowi bardziej czytelny rdzeń), możesz faktycznie przekazywać tablice (i odniesienia do zmiennych - ta funkcja działa jednak tylko w bash 4.3)! Ponadto wszystkie zmapowane zmienne są w zasięgu lokalnym, podobnie jak 1 USD (i inne).

Kod, który sprawia, że ​​ta praca jest dość lekka i działa zarówno w bash 3, jak i bash 4 (to jedyne wersje, z którymi go testowałem). Jeśli interesuje Cię więcej takich sztuczek, które sprawiają, że programowanie w bash jest znacznie przyjemniejsze i łatwiejsze, możesz rzucić okiem na moją platformę Bash Infinity Framework , poniższy kod został opracowany w tym celu.

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'
niieani
źródło
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=

Czy to pasuje?


źródło
cześć, próbowałem zrobić to jak twój, ale nie działa, przepraszam, że jestem nowicjuszem. Czy możesz mi powiedzieć, co jest nie tak z tym skryptem ?
ramgorur
1
Twoje użycie evaljest podatne na wykonanie dowolnego polecenia.
Chris Down,
Masz pomysł, aby evallinie Zhe były bezpieczniejsze? Zwykle, kiedy myślę, że potrzebuję eval, postanawiam nie używać * sh i zamiast tego przełączać się na inny język. Z drugiej strony, używając tego w skryptach, aby dołączyć wpisy do niektórych zmiennych podobnych do ŚCIEŻKI, będzie działać ze stałymi ciągami, a nie
1
możesz to zrobić evalbezpiecznie - ale to wymaga dużo myślenia. Jeśli próbujesz tylko odwołać się do parametru, chciałbyś zrobić coś takiego: w eval "$1=\"\$2\""ten sposób przy eval'spierwszym przejściu ocenia tylko 1 $, a przy drugim wartość = "2 $". Ale musisz zrobić coś innego - tutaj nie jest to konieczne.
mikeserv
Właściwie mój powyższy komentarz również jest błędny. Musisz to zrobić "${1##*[;"$IFS"]}=\"\$2\""- i nawet to nie ma gwarancji. Lub eval "$(set -- $1 ; shift $(($#-1)) ; echo $1)=\"\$2\"". To nie jest łatwe.
mikeserv
1

Nazwane argumenty po prostu nie są tym, jak zaprojektowano składnię Basha. Bash został zaprojektowany jako iteracyjne ulepszenie powłoki Bourne'a. Jako taki musi zapewnić, że pewne rzeczy działają między dwiema skorupami tak bardzo, jak to możliwe. Nie jest to więc łatwe do pisania skryptami, ale po prostu lepsze niż Bourne, przy jednoczesnym zapewnieniu, że przeniesienie skryptu ze środowiska Bourne bashjest tak proste, jak to możliwe. Nie jest to trywialne, ponieważ wiele pocisków wciąż traktuje Bourne'a jako de facto standard. Ponieważ ludzie piszą swoje skrypty, aby były kompatybilne z Bourne (dla tej przenośności), potrzeba ta pozostaje w mocy i jest mało prawdopodobne, aby się kiedykolwiek zmieniła.

Prawdopodobnie lepiej spojrzeć na inny skrypt powłoki (jak pythoncoś lub coś) całkowicie, jeśli jest to w ogóle wykonalne. Jeśli napotykasz ograniczenia związane z językiem, musisz zacząć używać nowego języka.

Bratchley
źródło
Może na wczesnym etapie bashżycia to była prawda. Ale teraz wprowadzono szczegółowe przepisy. Pełne zmienne odniesienia są teraz dostępne w bash 4.3- patrz odpowiedź deroberta .
Graeme,
A jeśli spojrzysz tutaj, zobaczysz, że możesz zrobić to naprawdę łatwo nawet za pomocą przenośnego kodu POSIX: unix.stackexchange.com/a/120531/52934
mikeserv
1

Przy standardowej shskładni (działałby bashi nie tylko bash) można wykonać:

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

Podobnie jak w przypadku rozwiązania z użyciem bash„s declare, jest bezpieczny , o ile $1zawiera poprawną nazwę zmiennej.

Stéphane Chazelas
źródło
0

NA NAZWANYCH ARGACH:

Jest to bardzo proste i bashwcale nie jest wymagane - jest to podstawowe zachowanie przypisania określone przez POSIX poprzez rozszerzenie parametrów:

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

Aby demonstrować w sposób podobny do @Graeme, ale w przenośny sposób:

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

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

I robię to tylko, str=aby upewnić się, że ma wartość zerową, ponieważ rozszerzenie parametru ma wbudowane zabezpieczenie przed ponownym przypisaniem środowiska powłoki, jeśli jest już ustawione.

ROZWIĄZANIE:

W przypadku konkretnego problemu nie uważam, że nazwane argumenty są konieczne, choć z pewnością są możliwe. $IFSZamiast tego użyj :

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
}

Oto, co dostaję, kiedy go uruchomię:

% 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

Zauważ, że dodało tylko argumenty, których jeszcze nie było $PATHlub które pojawiły się wcześniej? A może nawet że wymagało to więcej niż jednego argumentu? $IFSjest przydatny.

mikeserv
źródło
cześć, nie udało mi się podążać, więc czy mógłbyś rozwinąć to trochę więcej? dzięki.
ramgorur
Już to robię ... Jeszcze kilka chwil proszę ...
Mikeserv
@ramgorur Czy jest coś lepszego? Przepraszam, ale wtargnęło prawdziwe życie i zajęło mi to trochę dłużej, niż się spodziewałem, aby zakończyć pisanie.
mikeserv
to samo tutaj, uległo prawdziwemu życiu. Wygląda na to, że istnieje wiele różnych podejść do kodowania tego, pozwól mi dać czas na wybranie najlepszego.
ramgorur
@ramgorur - oczywiście, chciałem się tylko upewnić, że cię nie zawieszę. O tym - wybierasz, co chcesz, stary. Powiem, że żadna z pozostałych odpowiedzi, które widzę, nie jest tak zwięzła, przenośna ani tak solidne, jak assigntutaj. Jeśli masz pytania dotyczące tego, jak to działa, chętnie odpowiem. A tak przy okazji, jeśli naprawdę chcesz nazwanych argumentów, możesz spojrzeć na moją inną odpowiedź, w której pokazuję, jak zadeklarować funkcję nazwaną dla argumentów innej funkcji: unix.stackexchange.com/a/120531/52934
mikeserv
-2

Nie mogę znaleźć czegoś takiego jak rubin, python itp., Ale to jest mi bliższe

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

Moim zdaniem czytelność jest lepsza, 4 linie są zbyt duże, aby zadeklarować nazwy parametrów. Wygląda też bliżej współczesnych języków.

Francis Bongiovanni
źródło
(1) Pytanie dotyczy funkcji powłoki. Twoja odpowiedź przedstawia funkcję szkieletu. Poza tym twoja odpowiedź nie ma nic wspólnego z pytaniem. (2) Myślisz, że zwiększa to czytelność, biorąc oddzielne linie i łącząc je w jedną linię, z średnikami jako separatorami? Wierzę, że masz to wstecz; że styl jednowierszowy jest  mniej czytelny niż styl wieloliniowy.
Scott
Jeśli chodzi o redukcję niepotrzebnych części, to po co cytaty i średniki? a=$1 b=$2 ...działa równie dobrze.
ilkkachu