Eine Bash-Funktion, die wie andere Sprachen argumentiert?

17

Ich habe eine Bash-Funktion, um das $PATHwie folgt einzustellen -

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
}

Aber das Problem ist, ich muss unterschiedliche Funktionen für unterschiedliche Variablen schreiben (zum Beispiel eine andere Funktion für $CLASSPATHÄhnliches assign-classpath()usw.). Ich konnte keine Möglichkeit finden, Argumente an die Bash-Funktion zu übergeben, damit ich per Referenz darauf zugreifen kann.

Es wäre besser, wenn ich etwas hätte wie -

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

Irgendeine Idee, wie man so etwas in bash erreicht?

Ramgorur
quelle
Welche "anderen Sprachen"?
Choroba
Nun
1
assign-path /abcnicht anhängen /abczu PATH , wenn bereits $ PATH enthält /abc/def, /abcd, /def/abcetc. Besonders hinzufügen Sie können nicht , /binwenn PATH bereits enthält /usr/bin.
Miracle173
@ miracle173 - das wahr ist , was Sie brauchen , Split zu tun ist , $PATHund negieren Test gegen Ihre Argumente wie: add=/bin dir=/usr/bin ; [ -z "${dir%"$add"}" ] || dir="${dir}:${add}". In meiner Antwort mache ich es so mit so vielen Argumenten, wie Sie nur verwenden möchten IFS=:.
mikeserv
Bezogen auf das spezifische (Instanz-) Problem des Hinzufügens von Werten zu durch Doppelpunkte getrennten Listen: Wie kann ich sauber hinzufügen  $PATH?  und Verzeichnis hinzufügen, $PATHfalls es noch nicht vorhanden ist (unter Super User ).
Scott,

Antworten:

17

In können bashSie verwenden ${!varname}, um die Variable zu erweitern, auf die durch den Inhalt einer anderen verwiesen wird. Z.B:

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

Von der Manpage:

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

Sie können auch eine Variable festlegen, auf die durch den Inhalt verwiesen wird (ohne die Gefahren von eval) declare. Z.B:

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

So könnten Sie Ihre Funktion wie folgt schreiben (Vorsicht, wenn Sie sie declarein einer Funktion verwenden, müssen Sie angeben, -gsonst ist die Variable 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
}

Und benutze es wie folgt:

assign PATH /path/to/binaries

Beachten Sie, dass ich auch einen Fehler behoben habe, bei dem, wenn substrbereits eine Teilzeichenfolge eines durch Doppelpunkte getrennten Mitglieds bigstr, aber kein eigenes Mitglied vorhanden ist, diese nicht hinzugefügt wird. Dies würde beispielsweise das Hinzufügen /binzu einer PATHVariablen ermöglichen, die bereits enthält /usr/bin. Es verwendet die extglobMengen, um entweder den Anfang / das Ende der Zeichenfolge oder einen Doppelpunkt als irgendetwas anderes abzugleichen. Ohne extglobwäre die Alternative:

[[ $bigstr != $substr && $bigstr != *:$substr &&
   $bigstr != $substr:* && $bigstr != *:$substr:* ]]
Graeme
quelle
-gin declareist in der älteren version von bash nicht verfügbar, gibt es eine möglichkeit, diese abwärtskompatibel zu machen?
Ramgorur
2
@ramgorur, Sie könnten es verwenden export, um es in Ihre Umgebung zu stellen (auf die Gefahr hin, etwas Wichtiges zu überschreiben) oder eval(verschiedene Probleme, einschließlich der Sicherheit, wenn Sie nicht vorsichtig sind). Wenn evalSie es verwenden, sollten Sie in Ordnung sein, wenn Sie es mögen eval "$target=\$substr". Wenn Sie das \ allerdings vergessen , wird möglicherweise ein Befehl ausgeführt, wenn der Inhalt von ein Leerzeichen enthält substr.
Graeme
9

Neu in Bash 4.3 ist die -nOption zu declare& local:

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

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

Das druckt aus hello, world.

derobert
quelle
Das einzige Problem mit namerefs in Bash ist, dass Sie kein nameref (zum Beispiel in einer Funktion) haben können, das auf eine Variable (außerhalb der Funktion) mit demselben Namen wie das nameref selbst verweist. Dies kann jedoch für Release 4.5 behoben werden.
Kusalananda
2

Mit können Sie evaleinen Parameter einstellen. Eine Beschreibung dieses Befehls finden Sie hier . Die folgende Verwendung von evalist falsch:

falsch(){
  $ 1 = $ 2 auswerten
}

In Bezug auf die zusätzliche Bewertung evalsollten Sie verwenden

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

Überprüfen Sie die Ergebnisse der Verwendung dieser Funktionen:

$ X1 = '$ X2'
$ X2 = '$ X3'
$ X3 = 'xxx'
$ 
$ echo: $ X1:
: $ X2:
$ echo: $ X2:
: $ X3:
$ echo: $ X3:
: xxx:
$ 
$ falsch Y $ X1
$ echo: $ Y:
: $ X3:
$ 
$ weist Y $ X1 zu
$ echo: $ Y:
: $ X2:
$ 
$ weist Y "hallo world" zu
$ echo: $ Y:
: hallo welt:
$ # Folgendes kann unerwartet sein
$ weist Z $ Y zu
$ echo ": $ Z:"
:Hallo:
$ # Sie müssen also das zweite Argument in Anführungszeichen setzen, wenn es sich um eine Variable handelt
$ weist Z "$ Y" zu
$ echo ": $ Z:"
: hallo welt:

Aber Sie können Ihr Ziel ohne die Verwendung von erreichen eval. Ich bevorzuge diesen einfacheren Weg.

Die folgende Funktion macht die Substitution richtig (hoffe ich)

vermehren(){
  local CURRENT = $ 1
  lokales AUGMENT = $ 2
  lokal NEU
  if [[-z $ CURRENT]]; dann
    NEU = AUGMENT $
  elif [[! (($ CURRENT = $ AUGMENT) || ($ CURRENT = $ AUGMENT: *) || \
    ($ CURRENT = *: $ AUGMENT) || ($ CURRENT = *: $ AUGMENT: *))]]; dann
    NEW = $ CURRENT: $ AUGMENT
  sonst
    NEW = $ CURRENT
    fi
  Echo "$ NEW"
}

Überprüfen Sie die folgende Ausgabe

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
/Behälter


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
/Behälter:


Augment: / bin
::/Behälter


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

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

Jetzt können Sie die augmentFunktion folgendermaßen verwenden, um eine Variable festzulegen:

PATH = `Augment PATH / bin`
CLASSPATH = `CLASSPATH / bin erweitern`
LD_LIBRARY_PATH = `LD_LIBRARY_PATH / usr / lib` erweitern
miracle173
quelle
Sogar Ihre Bewertung ist falsch. Beispiel: v='echo "OHNO!" ; var' ; l=val ; eval $v='$l' - würde vor dem Zuweisen von var "OHNO!" Ausgeben. Sie könnten "$ {v ## * [;" $ IFS "]} = '$ l'", um sicherzustellen, dass die Zeichenfolge nicht zu etwas anderem erweitert werden kann wird nicht bewertet mit =.
mikeserv
@mikeserv, danke für deinen Kommentar, aber ich denke, das ist kein gültiges Beispiel. Das erste Argument des Assign-Skripts sollte ein Variablenname oder eine Variable sein, die einen Variablennamen enthält, der auf der linken Seite der =Assignment-Anweisung verwendet wird. Sie können argumentieren, dass mein Skript sein Argument nicht überprüft. Das ist wahr. Ich überprüfe nicht einmal, ob es ein Argument gibt oder ob die Anzahl der Argumente gültig ist. Aber das war beabsichtigt. Der OP kann solche Prüfungen hinzufügen, wenn er möchte.
Miracle173
@mikeserv: Ich denke, dass Ihr Vorschlag, das erste Argument stillschweigend in einen gültigen Variablennamen umzuwandeln, keine gute Idee ist: 1) Eine Variable wird gesetzt / überschrieben, die nicht vom Benutzer beabsichtigt war. 2) Der Fehler ist dem Benutzer der Funktion verborgen. Das ist niemals eine gute Idee. Man sollte einfach einen Fehler auslösen, wenn das passiert.
miracle173
@mikeserv: Es ist interessant, wenn man seine Variable v(besser seinen Wert) als zweites Argument der Assign-Funktion verwenden möchte . Sein Wert sollte sich also auf der rechten Seite einer Zuordnung befinden. Es ist notwendig, das Argument der Funktion zu zitieren assign. Ich habe diese Feinheit zu meinem Posting hinzugefügt.
miracle173
Wahrscheinlich stimmt das - und Sie verwenden in Ihrem letzten Beispiel nicht wirklich eval - was klug ist - es spielt also keine Rolle. Aber ich sage, dass jeder Code, der eval verwendet und Benutzereingaben nimmt, von Natur aus riskant ist - und wenn Sie Ihr Beispiel verwenden würden, könnte ich die Funktion so gestalten, dass mein Pfad mit geringem Aufwand in meinen Pfad geändert wird.
mikeserv
2

Mit ein paar Tricks können Sie benannte Parameter zusammen mit Arrays (getestet in Bash 3 und 4) an Funktionen übergeben.

Mit der von mir entwickelten Methode können Sie auf Parameter zugreifen, die an eine Funktion wie die folgende übergeben wurden:

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"

Mit anderen Worten, Sie können Ihre Parameter nicht nur nach ihrem Namen aufrufen (was einen besser lesbaren Kern ausmacht), sondern Sie können auch Arrays (und Verweise auf Variablen - diese Funktion funktioniert jedoch nur in Bash 4.3) übergeben! Außerdem befinden sich die zugeordneten Variablen alle im lokalen Bereich, ebenso wie $ 1 (und andere).

Der Code, mit dem dies funktioniert, ist ziemlich leicht und funktioniert sowohl in Bash 3 als auch in Bash 4 (dies sind die einzigen Versionen, mit denen ich es getestet habe). Wenn Sie an weiteren Tricks wie diesen interessiert sind, die das Entwickeln mit Bash viel schöner und einfacher machen, können Sie sich mein Bash Infinity Framework ansehen. Der folgende Code wurde zu diesem Zweck entwickelt.

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
quelle
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=

Passt das


quelle
Hallo, ich habe versucht, es so zu machen, aber es hat nicht funktioniert. Tut mir leid, dass ich ein Neuling bin. Können Sie mir bitte sagen, was mit diesem Skript nicht stimmt ?
Ramgorur
1
Ihre Verwendung von evalist anfällig für die Ausführung beliebiger Befehle.
Chris Down
Sie haben eine Idee, die evalLinien sicherer zu machen ? Normalerweise entscheide ich mich, wenn ich denke, dass ich eval brauche, nicht * sh zu verwenden und stattdessen zu einer anderen Sprache zu wechseln. Wenn Sie dies in Skripten verwenden, um Einträge an einige PATH-ähnliche Variablen anzuhängen, wird es mit Zeichenfolgenkonstanten und nicht mit Benutzereingaben ausgeführt ...
1
Sie können evalsicher tun - aber es erfordert viel Nachdenken. Wenn Sie nur versuchen, auf einen Parameter zu verweisen, möchten Sie Folgendes tun: Auf eval "$1=\"\$2\""diese Weise wird beim eval'sersten Durchgang nur 1 US-Dollar ausgewertet und beim zweiten Wert = "2 US-Dollar". Aber Sie müssen noch etwas tun - das ist hier nicht nötig.
mikeserv
Eigentlich ist mein obiger Kommentar auch falsch. Das musst du tun "${1##*[;"$IFS"]}=\"\$2\""- und selbst das kommt ohne Garantie. Oder eval "$(set -- $1 ; shift $(($#-1)) ; echo $1)=\"\$2\"". Es ist nicht einfach.
MikeServ
1

Benannte Argumente entsprechen einfach nicht der Syntax von Bash. Bash wurde als iterative Verbesserung der Bourne-Shell entwickelt. Als solches muss es sicherstellen, dass bestimmte Dinge so gut wie möglich zwischen den beiden Schalen funktionieren. So ist es nicht gemeint mit insgesamt leichter sein Skript, es ist nur gemeint , besser als Bourne sein , während das zu sichern , wenn Sie ein Skript von einer Bourne - Umgebung übernehmen , um bashes so einfach wie möglich ist. Das ist nicht trivial, da viele Shells Bourne immer noch als De-facto-Standard behandeln. Da die Benutzer ihre Skripte so schreiben, dass sie Bourne-kompatibel sind (für diese Portabilität), bleibt der Bedarf bestehen und wird sich wahrscheinlich nie ändern.

Sie sind wahrscheinlich besser dran, sich ein anderes Shell-Skript (wie pythonoder so) vollständig anzusehen, wenn es überhaupt machbar ist. Wenn Sie auf die Einschränkungen einer Sprache stoßen, müssen Sie eine neue Sprache verwenden.

Bratchley
quelle
Vielleicht war das schon früh so bash. Jetzt werden jedoch besondere Vorkehrungen getroffen. Vollständige Referenzvariablen sind jetzt in verfügbar bash 4.3- siehe die Antwort von derobert .
Graeme
Und wenn Sie hier reinschauen
mikeserv
1

Mit der Standardsyntax sh(würde funktionieren bashund nicht nur in bash) können Sie Folgendes tun:

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

Wie für Lösungen mit bash‚s declare, dann ist es sicher , solange $1ein gültigen Variablennamen enthält.

Stéphane Chazelas
quelle
0

AUF NAMED ARGS:

Dies ist sehr einfach und überhaupt bashnicht erforderlich - dies ist das grundlegende POSIX-festgelegte Zuweisungsverhalten über die Parametererweiterung:

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

So führen Sie eine Demo ähnlich wie bei @Graeme durch, jedoch auf tragbare Weise:

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

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

Und da mache ich nur, str=um sicherzustellen, dass es einen Nullwert hat, weil die Parametererweiterung den eingebauten Schutz gegen Neuzuweisung der Shell-Umgebung inline hat, wenn sie bereits gesetzt ist.

LÖSUNG:

Für Ihr spezifisches Problem glaube ich nicht, dass benannte Argumente notwendig sind, obwohl sie zweifellos möglich sind. Verwenden Sie $IFSstattdessen:

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
}

Folgendes bekomme ich, wenn ich es starte:

% 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

Beachten Sie, dass nur die Argumente hinzugefügt wurden, die noch nicht vorhanden waren $PATHoder die zuvor kamen? Oder sogar, dass es überhaupt mehr als ein Argument gab? $IFSist praktisch.

mikeserv
quelle
Hallo, ich bin nicht gefolgt. Könnten Sie es bitte etwas näher erläutern? Vielen Dank.
Ramgorur
Ich mache das schon ... Nur noch ein paar Momente bitte ...
mikeserv
@ Ramgorur Noch besser? Tut mir leid, aber das wirkliche Leben hat mich gestört und es hat ein bisschen länger gedauert, als ich erwartet hatte, um den Artikel fertig zu stellen.
mikeserv
das gleiche auch hier, dem wirklichen Leben erlegen. Sieht aus wie viele verschiedene Ansätze, um dieses Ding zu codieren. Lassen Sie mich etwas Zeit, um das beste zu finden.
Ramgorur
@ Ramgorur - natürlich wollte ich nur sicherstellen, dass ich dich nicht hängen lasse. Darüber - Sie wählen, was Sie wollen, Mann. Ich werde keine der anderen Antworten sagen, die ich als prägnante, tragbare oder so robuste Lösung wie assignhier sehen kann. Bei Fragen zur Funktionsweise stehe ich gerne zur Verfügung. Übrigens, wenn Sie wirklich benannte Argumente wollen, schauen Sie sich vielleicht meine andere Antwort an, in der ich zeige, wie man eine Funktion deklariert, die für die Argumente einer anderen Funktion benannt ist: unix.stackexchange.com/a/120531/52934
mikeserv
-2

Ich kann nichts wie Rubin, Python usw. finden, aber das kommt mir näher

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

Die Lesbarkeit ist meiner Meinung nach besser, 4 Zeilen reichen nicht aus, um Ihre Parameternamen zu deklarieren. Betrachtet auch moderne Sprachen näher.

Francis Bongiovanni
quelle
(1) Die Frage betrifft Shell-Funktionen. Ihre Antwort zeigt eine Skelettschalenfunktion. Darüber hinaus hat Ihre Antwort nichts mit der Frage zu tun. (2) Sie glauben, dass es die Lesbarkeit erhöht , einzelne Zeilen mit Semikolons als Trennzeichen zu einer Zeile zusammenzufassen? Ich glaube, Sie haben es verkehrt herum. dass Ihr einzeiliger Stil  weniger lesbar ist als der mehrzeilige Stil.
Scott
Wenn es darum geht, unnötige Teile zu reduzieren, warum dann die Anführungszeichen und Semikolons? a=$1 b=$2 ...funktioniert genauso gut.
Ilkkachu