Функція 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
}

Будь-яка ідея, як домогтися чогось подібного вище в баші?


Які "інші мови"?
choroba

добре, я намагався мати на увазі, якщо 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.
Graeme

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.
Kusalananda

2

Ви можете використовувати evalдля встановлення параметра. Опис цієї команди можна знайти тут . Таке використання evalневірно:

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

Щодо додаткової оцінки eval, яку ви повинні використовувати

призначити () {
  eval $ 1 = '$ 2'
}

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

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

Але ви можете досягти своєї мети без використання eval. Я віддаю перевагу такому простому способу.

Наступна функція робить підміну правильним способом (сподіваюся)

augment () {
  місцевий СУЧАСНИЙ = 1 долар
  місцевий ПОГОДА = 2 долари
  місцеві НОВІ
  якщо [[-z $ ТОЧНО]]; потім
    НОВИЙ = $ ПОГОДИ
  еліф [[! (($ CURRENT = $ AUGMENT) || ($ CURRENT = $ AUGMENT: *) || \
    ($ ТОЧНО = *: $ AUGMENT) || ($ CURRENT = *: $ AUGMENT: *))]]; потім
    НОВО = $ ТЕКУЩО: $ ПОГОДИ
  ще
    НОВО = $ ТОЧНО
    фі
  відлуння "$ НОВО"
}

Перевірте наступний вихід

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 / 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
/ бін:


збільшення: / бін
:: / бін


збільшити "/ 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 / бін"
LD_LIBRARY_PATH = `збільшення LD_LIBRARY_PATH / usr / lib`

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

@mikeserv, дякую за Ваш коментар, але я думаю, що це неправдивий приклад. Першим аргументом скрипту присвоєння має бути ім'я змінної або змінної, яка містить ім'я змінної, яке використовується з лівої сторони =оператора призначення. Ви можете стверджувати, що мій сценарій не перевіряє його аргумент. Це правда. Я навіть не перевіряю, чи є якийсь аргумент чи чи є кількість аргументів дійсними. Але це було за наміром. ОП може додати такі чеки, якщо захоче.
чудо173

@mikeserv: Я вважаю, що ваша пропозиція перетворити перший аргумент на безшумність у дійсне ім'я змінної - це не дуже гарна ідея: 1) встановлена ​​/ перезаписана якась змінна, яка не була призначена користувачем. 2) помилка прихована від користувача функції. Це ніколи не є хорошою ідеєю. Слід просто викликати помилку, якщо це трапиться.
чудо173

@mikeserv: Цікаво, коли хочеться використовувати вашу змінну v(краще її значення) як другий аргумент функції призначення. Таким чином, його значення має знаходитись праворуч від призначення. Необхідно цитувати аргумент функції 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лінії zhe більш безпечними? Зазвичай, коли я думаю, що мені потрібен eval, я вирішую не використовувати * sh і замість цього перейти на іншу мову. З іншого боку, використовуючи це в скриптах для додавання записів до змінних, подібних до PATH, воно буде працювати зі строковими константами, а не з введенням користувача ...

1
можна робити evalбезпечно - але це потребує багато думок. Якщо ви просто намагаєтесь посилатись на параметр, ви хочете зробити щось подібне: eval "$1=\"\$2\""при eval'sпершому проходженні він оцінює лише 1 долар, а на другому - значення = “$ 2”. Але потрібно зробити щось інше - це тут непотрібно.
mikeserv

Власне, і мій вище коментар є помилковим. Ви повинні робити "${1##*[;"$IFS"]}=\"\$2\""- і навіть це не має гарантії. Або eval "$(set -- $1 ; shift $(($#-1)) ; echo $1)=\"\$2\"". Це не легко.
mikeserv

1

Іменовані аргументи просто не так, як був розроблений синтаксис Баша. Bash був розроблений як ітеративне поліпшення на оболонці Борна. Як такий, йому потрібно максимально забезпечити роботу певних речей між двома снарядами. Отже, сценарій не має бути простішим в цілому, це просто бути кращим, ніж Борн, гарантуючи, що якщо взяти сценарій із середовища Борна, bashто це максимально просто. Це не банально, оскільки багато снарядів досі трактують Борна як фактичний стандарт. Оскільки люди пишуть свої сценарії як сумісні з Борном (для цієї портативності), потреба залишається в силі і навряд чи колись зміниться.

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


Можливо, на початку bashросійського життя це було правдою. Але зараз зроблено конкретні положення. Повні контрольні змінні тепер доступні у bash 4.3- див . Відповідь Дероберта .
Graeme

І якщо ви заглянете сюди, то побачите, що ви можете зробити подібні речі дуже легко навіть за допомогою лише портативного коду 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

Я не можу знайти нічого, як рубін, пітон тощо, але це відчуває мене ближче

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.