¿Una función bash que toma argumentos como otros idiomas?

17

Tengo una función bash para establecer $PATHasí:

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
}

Pero el problema es que tengo que escribir diferentes funciones para diferentes variables (por ejemplo, otra función para me $CLASSPATHgusta, assign-classpath()etc.). No pude encontrar una manera de pasar argumentos a la función bash para poder acceder a ella por referencia.

Sería mejor si tuviera algo como ...

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

Alguna idea, ¿cómo lograr algo como lo anterior en bash?

ramgorur
fuente
¿Qué "otros idiomas"?
choroba 01 de
bueno, traté de decir si bash permite "pasar por referencia" como en c / java etc.
ramgorur
1
assign-path /abcNo anexará /abcal PATH $ PATH si ya contiene /abc/def, /abcd, /def/abcetc. Sobre todo no se puede agregar /binsi la ruta ya contiene /usr/bin.
miracle173
@ miracle173 - esto es cierto, lo que hay que hacer es dividir $PATHy prueba negate contra sus argumentos como: add=/bin dir=/usr/bin ; [ -z "${dir%"$add"}" ] || dir="${dir}:${add}". En mi respuesta, lo hago de esta manera con tantos argumentos como quieras solo usando IFS=:.
mikeserv
Relacionado con el tema específico (instancia) de agregar valores a listas separadas por dos puntos: ¿Cómo puedo agregar limpiamente  $PATH?  y Agregar directorio a $PATHsi aún no está allí (en Super Usuario ).
Scott

Respuestas:

17

En bashpuede utilizar ${!varname}para expandir la variable referenciada por el contenido de otra. P.ej:

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

Desde la página del 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.

Además, para establecer una variable referenciada por los contenidos (sin los peligros de eval), puede usar declare. P.ej:

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

Por lo tanto, podría escribir su función de esta manera (tenga cuidado porque si usa declareuna función, debe dar -go la variable será local):

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
}

Y úsalo como:

assign PATH /path/to/binaries

Tenga en cuenta que también he corregido un error en el que si substrya es una subcadena de uno de los miembros separados por dos puntos bigstr, pero no su propio miembro, entonces no se agregaría. Por ejemplo, esto permitiría agregar /bina una PATHvariable que ya contiene /usr/bin. Utiliza los extglobconjuntos para hacer coincidir el principio / final de la cadena o dos puntos y luego cualquier otra cosa. Sin extglob, la alternativa sería:

[[ $bigstr != $substr && $bigstr != *:$substr &&
   $bigstr != $substr:* && $bigstr != *:$substr:* ]]
Graeme
fuente
-gin declareno está disponible en la versión anterior de bash, ¿hay alguna manera de que pueda hacer que esto sea compatible con versiones anteriores?
ramgorur
2
@ramgorur, puede usarlo exportpara ponerlo en su entorno (a riesgo de sobrescribir algo importante) o eval(varios problemas, incluida la seguridad, si no tiene cuidado). Si lo usa eval, debería estar bien si lo hace así eval "$target=\$substr". Sin \ embargo, si olvida esto, potencialmente ejecutará un comando si hay un espacio en el contenido de substr.
Graeme
9

Nuevo en bash 4.3, es la -nopción de declare& local:

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

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

Eso se imprime hello, world.

derobert
fuente
El único problema con namerefs en Bash es que no puede tener un nameref (en una función, por ejemplo) que haga referencia a una variable (fuera de la función) del mismo nombre que el nameref mismo. Sin embargo, esto puede solucionarse para la versión 4.5.
Kusalananda
2

Puede usar evalpara establecer un parámetro. Una descripción de este comando se puede encontrar aquí . El siguiente uso deeval es incorrecto:

incorrecto(){
  eval $ 1 = $ 2
}

Con respecto a la evaluación adicional eval, debe usar

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

Verifique los resultados del uso de estas funciones:

$ X1 = '$ X2'
$ X2 = '$ X3'
$ X3 = 'xxx'
PS 
$ echo: $ X1:
: $ X2:
$ echo: $ X2:
: $ X3:
$ echo: $ X3:
: xxx:
PS 
$ incorrecto Y $ X1
$ echo: $ Y:
: $ X3:
PS 
$ asignar Y $ X1
$ echo: $ Y:
: $ X2:
PS 
$ asignar Y "hola mundo"
$ echo: $ Y:
: hola mundo:
$ # lo siguiente puede ser inesperado
$ asignar Z $ Y
$ echo ": $ Z:"
:Hola:
$ # por lo que debe citar el segundo argumento si es una variable
$ asignar Z "$ Y"
$ echo ": $ Z:"
: hola mundo:

Pero puede lograr su objetivo sin el uso de eval. Prefiero esta manera que es más simple.

La siguiente función hace la sustitución de la manera correcta (espero)

aumentar(){
  CORRIENTE local = $ 1
  AUMENTO local = $ 2
  local NUEVO
  si [[-z $ CORRIENTE]]; luego
    NUEVO = $ AUGMENT
  elif [[! (($ CORRIENTE = $ AUGMENT) || ($ CORRIENTE = $ AUGMENT: *) || \
    ($ CORRIENTE = *: $ AUMENTO) || ($ CORRIENTE = *: $ AUGMENT: *))]]; luego
    NUEVO = $ CORRIENTE: $ AUMENTO
  más
    NUEVO = $ CORRIENTE
    fi
  echo "$ NUEVO"
}

Comprueba la siguiente salida

aumentar / usr / bin / bin
/ usr / bin: / bin

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

aumento / bin / bin
/compartimiento


aumentar / usr / bin: / bin
/ usr / bin :: / bin

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

aumento / bin: / bin
/compartimiento:


aumentar: / bin
::/compartimiento


aumentar "/ usr lib" "/ usr bin"
/ usr lib: / usr bin

aumentar "/ usr lib: / usr bin" "/ usr bin"
/ usr lib: / usr bin

Ahora puede usar la augmentfunción de la siguiente manera para establecer una variable:

RUTA = `aumentar RUTA / bin`
CLASSPATH = `aumentar CLASSPATH / bin`
LD_LIBRARY_PATH = `aumentar LD_LIBRARY_PATH / usr / lib`
milagro173
fuente
Incluso su declaración de evaluación es incorrecta. Esto, por ejemplo: v='echo "OHNO!" ; var' ; l=val ; eval $v='$l' - haría eco de "OHNO! Antes de asignar var. Puede" $ {v ## * [; "$ IFS"]} = '$ l' "para asegurarse de que la cadena no se puede expandir a nada que no será evaluado con el =.
mikeserv
@mikeserv gracias por tu comentario pero creo que no es un ejemplo válido. El primer argumento del script de asignación debe ser un nombre de variable o una variable que contenga un nombre de variable que se use en el lado izquierdo de la =declaración de asignación. Puede argumentar que mi script no verifica su argumento. Eso es verdad. Ni siquiera verifico si hay algún argumento o si el número de argumentos es válido. Pero esto fue por intención. El OP puede agregar tales controles si lo desea.
miracle173
@mikeserv: Creo que su propuesta de transformar el primer argumento de forma silenciosa en un nombre de variable válido no es una buena idea: 1) se establece / sobrescribe alguna variable que el usuario no pretendía. 2) el error está oculto para el usuario de la función. Esa nunca es una buena idea. Uno simplemente debería generar un error si eso sucede.
miracle173
@mikeserv: es interesante cuando uno quiere usar su variable v(mejor su valor) como segundo argumento de la función de asignación. Por lo tanto, su valor debe estar en el lado derecho de una asignación. Es necesario citar el argumento de la función assign. Agregué esta sutileza a mi publicación.
miracle173
Probablemente sea cierto, y en realidad no está utilizando eval en su ejemplo final, lo cual es inteligente, por lo que realmente no importa. Pero lo que digo es que cualquier código que use eval y tome la entrada del usuario es intrínsecamente riesgoso, y si usara su ejemplo, podría hacer que la función esté diseñada para cambiar mi ruta a rm con poco esfuerzo.
mikeserv
2

Con algunos trucos, puede pasar parámetros con nombre a funciones, junto con arreglos (probados en bash 3 y 4).

El método que desarrollé le permite acceder a los parámetros pasados ​​a una función como esta:

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"

En otras palabras, no solo puede llamar a sus parámetros por sus nombres (lo que compensa un núcleo más legible), sino que también puede pasar matrices (y referencias a variables; ¡esta función solo funciona en bash 4.3)! Además, todas las variables asignadas están en el ámbito local, igual que $ 1 (y otras).

El código que hace que esto funcione es bastante ligero y funciona tanto en bash 3 como en bash 4 (estas son las únicas versiones con las que lo he probado). Si está interesado en más trucos como este que hacen que desarrollar con bash sea mucho más fácil y fácil, puede echar un vistazo a mi Bash Infinity Framework , el código a continuación fue desarrollado para ese propósito.

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

¿Esto encaja?


fuente
hola, intenté hacerlo como el tuyo, pero no funcionó, lo siento por ser un novato. ¿Podría decirme qué hay de malo en este script ?
ramgorur 01 de
1
Su uso de evales susceptible a la ejecución arbitraria de comandos.
Chris Down
¿Tienes una idea para hacer que las evallíneas zhe sean más seguras? Normalmente, cuando creo que necesito evaluar, decido no usar * sh y cambiar a un idioma diferente. Por otro lado, al usar esto en scripts para agregar entradas a algunas variables similares a PATH, se ejecutará con constantes de cadena y no entrada del usuario ...
1
puede hacerlo de evalmanera segura, pero requiere mucha reflexión. Si solo está tratando de hacer referencia a un parámetro, querría hacer algo como esto: de eval "$1=\"\$2\""esa manera, en la eval'sprimera pasada, solo evalúa $ 1 y en la segunda, tiene valor = "$ 2". Pero debe hacer otra cosa, esto es innecesario aquí.
mikeserv
En realidad, mi comentario anterior también está mal. Tienes que hacerlo "${1##*[;"$IFS"]}=\"\$2\"", e incluso eso no tiene garantía. O eval "$(set -- $1 ; shift $(($#-1)) ; echo $1)=\"\$2\"". No es fácil.
mikeserv
1

Los argumentos con nombre simplemente no son cómo se diseñó la sintaxis de Bash. Bash fue diseñado para ser una mejora iterativa sobre el shell Bourne. Como tal, debe asegurarse de que ciertas cosas funcionen entre los dos depósitos tanto como sea posible. Por lo tanto, no está destinado a ser más fácil de escribir en general, solo está destinado a ser mejor que Bourne al tiempo que garantiza que si lleva un script de un entorno Bourne a bashlo más fácil posible. Eso no es trivial ya que muchos proyectiles todavía tratan a Bourne como un estándar de facto. Dado que las personas escriben sus scripts para que sean compatibles con Bourne (para esta portabilidad), la necesidad sigue vigente y es poco probable que cambie.

Probablemente sea mejor mirar un script de shell diferente (como pythono algo) por completo si es factible. Si se encuentra con las limitaciones de un idioma, debe comenzar a usar un nuevo idioma.

Bratchley
fuente
Quizás temprano en bashla vida esto era cierto. Pero ahora se hacen provisiones específicas. Las variables de referencia completas ahora están disponibles en bash 4.3- vea la respuesta de derobert .
Graeme
Y si mira aquí, verá que puede hacer este tipo de cosas realmente fácilmente incluso con el código portátil POSIX: unix.stackexchange.com/a/120531/52934
mikeserv
1

Con la shsintaxis estándar (funcionaría en bash, y no solo en bash), podría hacer:

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

Al igual que para las soluciones que usan bash's declare, es seguro siempre que $1contenga un nombre de variable válido.

Stéphane Chazelas
fuente
0

EN ARGOS NOMBRADOS:

Esto se hace de manera muy simple y bashno se requiere en absoluto: este es el comportamiento básico de asignación especificado por POSIX a través de la expansión de parámetros:

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

Para hacer una demostración de una manera similar a @Graeme, pero de forma portátil:

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

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

Y allí solo hago str=para asegurarme de que tenga un valor nulo, porque la expansión de parámetros tiene la protección incorporada contra la reasignación del entorno de shell en línea si ya está configurado.

SOLUCIÓN:

Para su problema específico, no creo que los argumentos nombrados sean necesarios, aunque ciertamente son posibles. Use en su $IFSlugar:

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
}

Esto es lo que obtengo cuando lo ejecuto:

% 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

¿Notó que solo agregó los argumentos que aún no estaban incluidos $PATHo que aparecieron antes? ¿O incluso que tomó más de un argumento? $IFSes útil

mikeserv
fuente
hola, no lo seguí, así que ¿podrías explicarlo un poco más? Gracias.
ramgorur 01 de
Ya lo estoy haciendo ... Solo unos momentos más por favor ...
mikeserv
@ramgorur ¿Algo mejor? Lo siento, pero la vida real se entrometió y me llevó un poco más de tiempo de lo que esperaba terminar el artículo.
mikeserv
lo mismo aquí también, sucumbió a la vida real. Parece que hay muchos enfoques diferentes para codificar esta cosa, déjame darme un momento para decidir cuál es el mejor.
ramgorur
@ramgorur, por supuesto, solo quería asegurarme de que no te dejara colgado. Acerca de esto: eliges lo que quieras, hombre. No diré que ninguna de las otras respuestas que veo ofrecen una solución tan concisa, portátil o tan robusta como assignaquí. Si tiene preguntas sobre cómo funciona, me complacería responder. Y, por cierto, si realmente quieres argumentos con nombre, quizás quieras ver esta otra respuesta mía en la que muestro cómo declarar una función nombrada para los argumentos de otra función: unix.stackexchange.com/a/120531/52934
mikeserv
-2

No puedo encontrar nada como rubí, pitón, etc., pero esto se siente más cerca de mí.

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

La legibilidad es mejor en mi opinión, 4 líneas son excesivas para declarar los nombres de sus parámetros. También se parece más a los idiomas modernos.

Francis Bongiovanni
fuente
(1) La pregunta es sobre las funciones de shell. Su respuesta presenta una función de esqueleto. Más allá de eso, su respuesta no tiene nada que ver con la pregunta. (2) ¿Crees que aumenta la legibilidad tomar líneas separadas y concatenarlas en una sola línea, con punto y coma como separadores? Creo que lo tienes al revés; que su estilo de una línea es  menos legible que el estilo de varias líneas.
Scott
Si el objetivo es reducir partes innecesarias, ¿por qué las comillas y los puntos y comas? a=$1 b=$2 ...funciona igual de bien
ilkkachu