Функция bash, которая принимает аргумент как другие языки?


17

У меня есть функция Bash, чтобы установить, $PATHкак это -

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
}

Но проблема в том, что мне нужно написать разные функции для разных переменных (например, другую функцию для $CLASSPATHподобных assign-classpath()и т. Д.). Я не смог найти способ передать аргумент в функцию bash, чтобы я мог получить к нему доступ по ссылке.

Было бы лучше, если бы у меня было что-то вроде -

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

Есть идеи, как добиться чего-то похожего в bash?


Какие "другие языки"?
Чороба

хорошо, я пытался иметь в виду, если bash позволяет "передавать по ссылке", как в c / java и т. д.
ramgorur

1
assign-path /abcне будет добавлять /abcк PATH , если $ PATH уже содержит /abc/def, /abcd, и /def/abcт.д. Особенно вы не можете добавить , /binесли PATH уже содержит /usr/bin.
чудо173

@ miracle173 - это правда, что вам нужно сделать , это разделение $PATHи свести на нет теста на ваши аргументы , как: add=/bin dir=/usr/bin ; [ -z "${dir%"$add"}" ] || dir="${dir}:${add}". В своем ответе я делаю это таким образом, используя столько аргументов, сколько вы хотите использовать IFS=:.
mikeserv

В связи со специфической (экземплярной) проблемой добавления значений в списки, разделенные двоеточиями: Как я могу чисто добавить в  $PATH?  и добавьте каталог, $PATHесли его там еще нет (на Super User ).
Скотт

Ответы:


17

В bashвы можете использовать ${!varname}для раскрытия переменной, на которую ссылается содержимое другой. Например:

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

Со страницы руководства:

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

Кроме того eval, вы можете использовать переменную, на которую ссылается содержимое (без опасностей ) declare. Например:

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

Таким образом, вы могли бы написать свою функцию следующим образом ( будьте осторожны, потому что если вы используете declareфункцию, вы должны указать, -gили переменная будет локальной):

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
}

И используйте это как:

assign PATH /path/to/binaries

Обратите внимание, что я также исправил ошибку, из-за которой, если substrона уже является подстрокой одного из членов, разделенных двоеточиями bigstr, но не является собственным, то она не будет добавлена. Например, это позволит добавить /binв PATHпеременную, уже содержащую /usr/bin. Он использует extglobнаборы, чтобы соответствовать либо началу / концу строки, либо двоеточию, чем чему-либо еще. Без extglob, альтернатива будет:

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

-gin declareнедоступен в более старой версии bash, есть ли способ сделать его обратно совместимым?
ramgorur

2
@ramgorur, вы можете использовать его, exportчтобы поместить его в свою среду (риск перезаписать что-то важное) или eval(различные вопросы, включая безопасность, если вы не будете осторожны). При использовании evalвы должны быть в порядке, если вы делаете это как eval "$target=\$substr". Если вы забудете об этом \ , он потенциально выполнит команду, если в содержимом будет пробел substr.
Грэм

9

Новое в bash 4.3, это -nопция declare& local:

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

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

Это распечатывает hello, world.


Единственная проблема с namerefs в Bash заключается в том, что вы не можете иметь nameref (в функции, например), ссылающуюся на переменную (вне функции) с тем же именем, что и сама nameref. Это может быть исправлено в версии 4.5, хотя.
Кусалананда

2

Вы можете использовать evalдля установки параметра. Описание этой команды можно найти здесь . Следующее использование evalневерно:

неправильно(){
  eval $ 1 = $ 2
}

Что касается дополнительной оценки eval, следует ли вам использовать

правопреемник () {
  eval $ 1 = '$ 2'
}

Проверьте результаты использования этих функций:

$ X1 = '$ X2'
$ X2 = '$ X3'
$ X3 = 'ххх'
$ 
$ echo: $ X1:
: $ X2:
$ echo: $ X2:
: $ X3:
$ echo: $ X3:
: Ххх:
$ 
$ неправильно Y $ X1
$ echo: $ Y:
: $ X3:
$ 
$ assign Y $ X1
$ echo: $ Y:
: $ X2:
$ 
$ assign Y "Привет, мир"
$ echo: $ Y:
: hallo world:
$ # следующее может быть неожиданным
$ assign Z $ Y
$ echo ": $ Z:"
: Hallo:
$ #, поэтому вы должны заключить в кавычки второй аргумент, если это переменная
$ assign Z "$ Y"
$ echo ": $ Z:"
: hallo world:

Но вы можете достичь своей цели без использования eval. Я предпочитаю этот способ, который является более простым.

Следующая функция делает замену правильным образом (надеюсь)

Augment () {
  местный ТОК = 1 $
  местный АВГУМЕНТ = 2 $
  местный NEW
  if [[-z $ CURRENT]]; тогда
    NEW = $ увеличивающие
  элиф [[! (($ CURRENT = $ AUGMENT) || ($ CURRENT = $ AUGMENT: *) || \
    ($ CURRENT = *: $ AUGMENT) || ($ CURRENT = *: $ AUGMENT: *))]]; тогда
    NEW = $ CURRENT: $ увеличивающие
  еще
    NEW = $ ТОК
    фи
  echo "$ NEW"
}

Проверьте следующий вывод

augment / usr / bin / bin
/ USR / бен: / бен

augment / usr / bin: / bin / bin
/ USR / бен: / бен

augment / usr / bin: / bin: / usr / local / bin / bin
/ USR / бен: / бен: / USR / местные / бен

augment / bin: / usr / bin / bin
/ Бен: / USR / бен

аугмент / бин / бин
/ бен


augment / usr / bin: / bin
/ USR / бен :: / бен

augment / usr / bin: / bin: / bin
/ USR / бен: / бен:

augment / usr / bin: / bin: / usr / local / bin: / bin
/ USR / бен: / бен: / USR / местные / бен:

augment / bin: / usr / bin: / bin
/ Бен: / USR / бен:

augment / bin: / bin
/ Бен:


увеличить: / bin
:: / бен


увеличить "/ usr lib" "/ usr bin"
/ usr lib: / usr bin

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

Теперь вы можете использовать augmentфункцию следующим образом для установки переменной:

PATH = `увеличить PATH / bin`
CLASSPATH = `увеличить CLASSPATH / bin`
LD_LIBRARY_PATH = `увеличить LD_LIBRARY_PATH / usr / lib`

Даже ваше неверное утверждение неверно. Это, например: v='echo "OHNO!" ; var' ; l=val ; eval $v='$l' - выдает «OHNO!» Перед назначением переменной. Вы можете «$ {v ## * [;« $ IFS »]} = '$ l'", чтобы гарантировать, что строка не может быть расширена до чего-либо, что не будет оцениваться с =.
mikeserv

@mikeserv спасибо за ваш комментарий, но я думаю, что это неверный пример. Первым аргументом сценария присвоения должно быть имя переменной или переменная, которая содержит имя переменной, которое используется в левой части =оператора присваивания. Вы можете утверждать, что мой скрипт не проверяет свои аргументы. Это правда. Я даже не проверяю, есть ли какой-либо аргумент или число аргументов является действительным. Но это было намеренно. ОП может добавить такие проверки, если он хочет.
чудо173

@mikeserv: Я думаю, что ваше предложение тихо преобразовать первый аргумент в правильное имя переменной не является хорошей идеей: 1) установлена ​​/ перезаписана некоторая переменная, которая не была предназначена пользователем. 2) ошибка скрыта от пользователя функции. Это никогда не хорошая идея. Нужно просто поднять ошибку, если это произойдет.
чудо173

@mikeserv: Интересно, когда кто-то хочет использовать вашу переменную v(лучше ее значение) в качестве второго аргумента функции assign. Таким образом, его значение должно быть справа от назначения. Необходимо процитировать аргумент функции assign. Я добавил эту тонкость в мою публикацию.
чудо173

Вероятно, правда - и вы на самом деле не используете eval в своем последнем примере - что мудро - так что это не имеет значения. Но я хочу сказать, что любой код, использующий eval и принимающий пользовательский ввод, по своей сути рискован - и если бы вы использовали ваш пример, я мог бы сделать функцию, предназначенную для изменения моего пути, чтобы изменить мой путь без особых усилий.
mikeserv

2

С помощью нескольких приемов вы можете фактически передать именованные параметры в функции вместе с массивами (протестировано в bash 3 и 4).

Метод, который я разработал, позволяет получить доступ к параметрам, переданным в функцию, подобную этой:

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"

Другими словами, вы можете не только вызывать параметры по их именам (что делает ядро ​​более читабельным), но и передавать массивы (и ссылки на переменные - эта функция работает только в bash 4.3)! Кроме того, все отображаемые переменные находятся в локальной области, как и 1 доллар (и другие).

Код, который делает эту работу, довольно легкий и работает как в bash 3, так и в bash 4 (это единственные версии, с которыми я его тестировал). Если вас интересуют другие подобные хитрости, которые делают разработку с помощью bash намного приятнее и проще, вы можете взглянуть на мою среду Bash Infinity Framework , для которой был разработан приведенный ниже код.

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=

Это подходит?


привет, я пытался сделать это как у тебя, но не работал, извините за то, что новичок. Не могли бы вы сказать мне, что не так с этим сценарием ?
ramgorur

1
Ваше использование evalподвержено произвольному выполнению команды.
Крис Даун

У вас есть идея, чтобы сделать evalлинии более безопасными? Обычно, когда я думаю, что мне нужен eval, я решаю не использовать * sh и переключаться на другой язык. С другой стороны, используя это в scrips для добавления записей в некоторые переменные, подобные PATH, он будет работать со строковыми константами, а не с пользовательским вводом ...

1
Вы можете сделать это evalбезопасно - но это требует много мыслей. Если вы просто пытаетесь сослаться на параметр, вы захотите сделать что-то вроде этого: eval "$1=\"\$2\""при eval'sпервом проходе он оценивает только $ 1, а при втором - value = "$ 2". Но вам нужно сделать что-то еще - здесь это не нужно.
mikeserv

На самом деле мой вышеприведенный комментарий тоже неверен. Вы должны сделать "${1##*[;"$IFS"]}=\"\$2\""- и даже это идет без гарантии. Или eval "$(set -- $1 ; shift $(($#-1)) ; echo $1)=\"\$2\"". Это не легко.
mikeserv

1

Именованные аргументы просто не соответствуют синтаксису Bash. Bash был разработан для итеративного улучшения оболочки Bourne. Как таковой, он должен гарантировать, что определенные вещи работают между двумя оболочками в максимально возможной степени. Таким образом, это не означает, что в целом сценарий проще, он просто лучше, чем Борн, и при этом гарантирует, что если вы перенесете сценарий из среды Борна в bashмаксимально простой режим . Это не тривиально, так как многие оболочки по-прежнему рассматривают Борн как стандарт де-факто. Поскольку люди пишут свои сценарии для Bourne-совместимости (для этой мобильности), потребность остается в силе и вряд ли когда-либо изменится.

Возможно, вам лучше рассмотреть другой сценарий оболочки (например, pythonили что-то еще) полностью, если это вообще возможно. Если вы сталкиваетесь с ограничениями языка, вам нужно начать использовать новый язык.


Возможно, в начале bashнашей жизни это было правдой. Но сейчас конкретные положения сделаны. Полные справочные переменные теперь доступны в bash 4.3- см . Ответ Дероберта .
Грэм,

И если вы посмотрите сюда, вы увидите, что вы действительно можете легко делать подобные вещи даже с помощью просто переносимого кода POSIX: unix.stackexchange.com/a/120531/52934
mikeserv

1

Со стандартным shсинтаксисом (будет работать в bash, а не только в bash), вы можете сделать:

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

Как и для решений, использующих bash«s» declare, это безопасно, если $1содержит допустимое имя переменной.


0

НА ИМЕННЫХ АРГАХ:

Это очень просто сделать и bashне нужно вообще - это базовое POSIX-поведение при назначении через расширение параметров:

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

Для демонстрации в том же духе, что и в @Graeme, но в переносном виде:

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

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

И там я только делаю str= чтобы убедиться, что оно имеет нулевое значение, потому что расширение параметра имеет встроенную защиту от переназначения встроенной среды оболочки, если оно уже установлено.

РЕШЕНИЕ:

Для вашей конкретной проблемы я не считаю именованные аргументы необходимыми, хотя они, безусловно, возможны. Используйте $IFSвместо:

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
}

Вот что я получаю, когда запускаю его:

% 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

Заметьте, что это только добавило аргументы, которых еще не было $PATHили которые были раньше? Или даже, что потребовалось больше, чем один аргумент? $IFSудобно


привет, я не смог подписаться, поэтому не могли бы вы рассказать об этом немного подробнее? Благодарю.
ramgorur

Я уже делаю это ... Еще несколько минут, пожалуйста ...
mikeserv

@ramgorur Что лучше? Извините, но реальная жизнь вторглась, и мне потребовалось немного больше времени, чем я ожидал, чтобы закончить рецензию.
mikeserv

то же самое и здесь, поддаваясь реальной жизни. Похоже, много разных подходов для кодирования этой вещи, позвольте мне дать мне время остановиться на лучшем.
ramgorur

@ramgorur - конечно, просто хотел убедиться, что я не оставлю тебя в покое. Об этом - выбирай, что хочешь, мужик. Я не скажу, что ни один из других ответов, которые я могу предложить, является кратким, переносимым или столь же надежным, как assignздесь. Если у вас есть вопросы о том, как это работает, я с удовольствием отвечу. И, кстати, если вы действительно хотите именованные аргументы, вы можете посмотреть на этот мой другой ответ, в котором я покажу, как объявить функцию, названную для аргументов другой функции: unix.stackexchange.com/a/120531/52934
mikeserv

-2

Я не могу найти ничего похожего на ruby, python и т. Д., Но мне это кажется ближе

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

На мой взгляд, читаемость лучше, 4 строки избыточны для объявления имен ваших параметров. Смотрится ближе к современным языкам.


(1) Вопрос касается функций оболочки. Ваш ответ представляет функцию оболочки скелета. Кроме того, ваш ответ не имеет ничего общего с вопросом. (2) Вы думаете, что это повышает удобочитаемость, беря отдельные строки и объединяя их в одну строку, используя точки с запятой в качестве разделителей? Я верю, что вы получили это задом наперед; что ваш однострочный стиль  менее читабелен, чем многострочный.
Скотт

Если смысл в сокращении ненужных частей, то почему кавычки и точки с запятой? a=$1 b=$2 ...работает так же хорошо.
ilkkachu
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.