Як змінити глобальну змінну в межах функції в bash?


104

Я працюю з цим:

GNU bash, version 4.1.2(1)-release (x86_64-redhat-linux-gnu)

У мене є такий сценарій, як нижче:

#!/bin/bash

e=2

function test1() {
  e=4
  echo "hello"
}

test1 
echo "$e"

Що повертає:

hello
4

Але якщо я призначу результат функції змінній, глобальна змінна eне змінюється:

#!/bin/bash

e=2

function test1() {
  e=4
  echo "hello"
}

ret=$(test1)

echo "$ret"
echo "$e"

Повернення:

hello
2

Я чув про використання eval в цьому випадку, тому зробив це в test1:

eval 'e=4'

Але такий же результат.

Чи можете ви пояснити мені, чому він не модифікований? Як я можу зберегти відлуння test1функції в retі змінити також глобальну змінну?


Вам потрібно повернутися привіт? Ви можете просто відлучити $ e, щоб він повернувся. Або повторювати все, що ви хочете, а потім проаналізувати результат?

Відповіді:


98

Коли ви використовуєте підстановку команд (тобто $(...)конструкцію), ви створюєте піддіаграма. Subhells успадковує змінні зі своїх батьківських оболонок, але це працює лише одним способом - subhell не може змінювати середовище своєї батьківської оболонки. Ваша змінна eвстановлюється в межах підрозділу, але не в батьківській оболонці. Існує два способи передачі значень з підшару в його батьківський. Спочатку ви можете вивести щось у stdout, а потім зафіксувати його за допомогою підстановки команд:

myfunc() {
    echo "Hello"
}

var="$(myfunc)"

echo "$var"

Дає:

Hello

Для числового значення від 0-255 ви можете returnпередати число як статус виходу:

mysecondfunc() {
    echo "Hello"
    return 4
}

var="$(mysecondfunc)"
num_var=$?

echo "$var - num is $num_var"

Дає:

Hello - num is 4

Дякую за точку, але я повинен повернути рядковий масив, і в межах функції я повинен додати елементи до двох глобальних рядкових масивів.
harrison4

3
Ви розумієте, що якщо ви просто запустите функцію, не призначивши її змінній, усі глобальні змінні в ній оновляться. Замість того, щоб повертати рядковий масив, чому б не просто оновити рядковий масив у функції, а потім призначити його іншій змінній після закінчення функції?

@JohnDoe: Ви не можете повернути "рядковий масив" з функції. Все, що ви можете зробити - це надрукувати рядок. Однак ви можете зробити щось подібне:setarray() { declare -ag "$1=(a b c)"; }
rici

34

Для цього потрібно bash 4.1, якщо ви використовуєте {fd}або local -n.

Решта повинні працювати в баш 3.x, сподіваюся. Я не зовсім впевнений через те, що printf %qце може бути функцією 4.

Підсумок

Ваш приклад можна змінити так, щоб архівувати потрібний ефект:

# Add following 4 lines:
_passback() { while [ 1 -lt $# ]; do printf '%q=%q;' "$1" "${!1}"; shift; done; return $1; }
passback() { _passback "$@" "$?"; }
_capture() { { out="$("${@:2}" 3<&-; "$2_" >&3)"; ret=$?; printf "%q=%q;" "$1" "$out"; } 3>&1; echo "(exit $ret)"; }
capture() { eval "$(_capture "$@")"; }

e=2

# Add following line, called "Annotation"
function test1_() { passback e; }
function test1() {
  e=4
  echo "hello"
}

# Change following line to:
capture ret test1 

echo "$ret"
echo "$e"

друкується за бажанням:

hello
4

Зауважте, що це рішення:

  • Працює і для e=1000.
  • Консервує, $?якщо вам потрібно$?

Єдиними поганими побічними ефектами є:

  • Це потрібно сучасне bash.
  • Він роздрібнюється досить часто.
  • Потрібна примітка (названа по вашій функції, із додаванням _)
  • Він жертвує дескриптором файлів 3.
    • Ви можете змінити його на інший FD, якщо вам це потрібно.
      • У _captureпросто замінити всі місця де 3з іншого (вище) числом.

Наступне (що досить довго, вибачте за це), сподіваємось, пояснює, як віднести цей рецепт до інших сценаріїв.

Проблема

d() { let x++; date +%Y%m%d-%H%M%S; }

x=0
d1=$(d)
d2=$(d)
d3=$(d)
d4=$(d)
echo $x $d1 $d2 $d3 $d4

виходи

0 20171129-123521 20171129-123521 20171129-123521 20171129-123521

поки потрібний вихід

4 20171129-123521 20171129-123521 20171129-123521 20171129-123521

Причина проблеми

Змінні оболонки (або взагалі кажучи, оточення) передаються від батьківських процесів до дочірніх процесів, але не навпаки.

Якщо ви займаєтеся захопленням виводу, зазвичай це запускається в підпакеті, тому передавати змінні назад складно.

Деякі навіть вам кажуть, що це неможливо виправити. Це неправильно, але давно відому важко вирішити проблему.

Існує кілька способів, як це найкраще вирішити, це залежить від ваших потреб.

Ось покрокове керівництво про те, як це зробити.

Передача змінних в батьківську оболонку

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

_passback() { while [ 0 -lt $# ]; do printf '%q=%q;' "$1" "${!1}"; shift; done; }

d() { let x++; d=$(date +%Y%m%d-%H%M%S); _passback x d; }

x=0
eval `d`
d1=$d
eval `d`
d2=$d
eval `d`
d3=$d
eval `d`
d4=$d
echo $x $d1 $d2 $d3 $d4

відбитки

4 20171129-124945 20171129-124945 20171129-124945 20171129-124945

Зауважте, що це працює і для небезпечних речей:

danger() { danger="$*"; passback danger; }
eval `danger '; /bin/echo *'`
echo "$danger"

відбитки

; /bin/echo *

Це пов'язано з тим printf '%q', що цитує все таке, що ви можете повторно використовувати його в контексті оболонки.

Але це біль у ..

Це не тільки виглядає некрасиво, воно також багато набирає, тому воно схильне до помилок. Лише одна єдина помилка, і ви приречені, правда?

Ну, ми на рівні оболонки, тож ви можете вдосконалити її. Подумайте лише про інтерфейс, який ви хочете бачити, і тоді ви зможете його реалізувати.

Додайте, як оболонка обробляє речі

Повернемось крок назад і подумаємо про якийсь API, який дозволяє нам легко висловити, що ми хочемо зробити.

Ну, що ми хочемо робити з d()функцією?

Ми хочемо захопити результат у змінну. Гаразд, тоді давайте реалізуємо API саме для цього:

# This needs a modern bash 4.3 (see "help declare" if "-n" is present,
# we get rid of it below anyway).
: capture VARIABLE command args..
capture()
{
local -n output="$1"
shift
output="$("$@")"
}

Тепер замість того, щоб писати

d1=$(d)

ми можемо писати

capture d1 d

Ну, це виглядає так, що ми не сильно змінилися, оскільки, знову ж таки, змінні не передаються назад dу батьківську оболонку, і нам потрібно набрати ще трохи.

Однак тепер ми можемо кинути на нього всю потужність оболонки, оскільки вона гарно укутана у функцію.

Подумайте про легкий для повторного використання інтерфейс

Друга річ - це те, що ми хочемо бути СУХОМИ (не повторюйте себе). Тож ми остаточно не хочемо набирати щось подібне

x=0
capture1 x d1 d
capture1 x d2 d
capture1 x d3 d
capture1 x d4 d
echo $x $d1 $d2 $d3 $d4

xТут не тільки зайвим, це значною кількістю помилок завжди repeate в правильному контексті. Що робити, якщо ви використовуєте його 1000 разів у сценарії, а потім додаєте змінну? Ви, безумовно, не хочете змінювати всі 1000 місць, в dяких бере участь виклик .

Тож залишайте xподалі, щоб ми могли написати:

_passback() { while [ 0 -lt $# ]; do printf '%q=%q;' "$1" "${!1}"; shift; done; }

d() { let x++; output=$(date +%Y%m%d-%H%M%S); _passback output x; }

xcapture() { local -n output="$1"; eval "$("${@:2}")"; }

x=0
xcapture d1 d
xcapture d2 d
xcapture d3 d
xcapture d4 d
echo $x $d1 $d2 $d3 $d4

виходи

4 20171129-132414 20171129-132414 20171129-132414 20171129-132414

Це вже виглядає дуже добре. (Але все ж є те, local -nщо не працює в звичайному bash3.x)

Уникайте змін d()

Останнє рішення має деякі великі вади:

  • d() потребує змін
  • xcaptureДля передачі результату потрібно використовувати деякі внутрішні деталі .
    • Зауважте, що ця тінь (спалює) одну змінну з назвою output, тому ми її ніколи не можемо повернути назад.
  • З цим потрібно співпрацювати _passback

Чи можемо ми також позбутися цього?

Звичайно, ми можемо! Ми перебуваємо в оболонці, тому є все необхідне для цього.

Якщо ви придивитесь трохи ближче до дзвінка, evalто можете побачити, що ми маємо 100% контроль у цьому місці. "Всередині" evalми перебуваємо в нижній частині, тому ми можемо робити все, що хочемо, не побоюючись зробити щось погане батьківській оболонці.

Так, добре, так що давайте додамо ще одну обгортку, зараз прямо всередині eval:

_passback() { while [ 0 -lt $# ]; do printf '%q=%q;' "$1" "${!1}"; shift; done; }
# !DO NOT USE!
_xcapture() { "${@:2}" > >(printf "%q=%q;" "$1" "$(cat)"); _passback x; }  # !DO NOT USE!
# !DO NOT USE!
xcapture() { eval "$(_xcapture "$@")"; }

d() { let x++; date +%Y%m%d-%H%M%S; }

x=0
xcapture d1 d
xcapture d2 d
xcapture d3 d
xcapture d4 d
echo $x $d1 $d2 $d3 $d4

відбитки

4 20171129-132414 20171129-132414 20171129-132414 20171129-132414                                                    

Однак це, знову ж таки, має певний вагомий недолік:

  • У !DO NOT USE!фломастери є, тому що є дуже поганий стан гонки в цьому, що ви не можете легко бачити:
    • Це >(printf ..)фонове завдання. Тож воно все ще може виконуватися під час _passback xзапуску.
    • Ви можете переконатись у цьому самі, якщо додати sleep 1;до printfабо _passback. _xcapture a d; echoпотім виходи xабо aспочатку відповідно.
  • Це _passback xне повинно бути частиною _xcapture, оскільки це ускладнює повторне використання цього рецепту.
  • Також у нас є декілька безпілотних вилок (але $(cat)), але, як це рішення, !DO NOT USE!я взяв найкоротший маршрут.

Однак це показує, що ми можемо це зробити, не змінюючи d()(і без local -n)!

Зверніть увагу, що нам це необов'язково потрібно _xcapture, як це можна було б написати в кожному правильному eval.

Однак зазвичай це не дуже читабельно. Якщо ви повернетесь до свого сценарію через кілька років, ви, мабуть, захочете знову прочитати його без особливих проблем.

Зафіксуйте гонку

Тепер давайте виправимо умову гонки.

Хитрість може полягати в тому, щоб зачекати, поки він printfне закриється, і потім вивести його x.

Існує багато способів архівувати це:

  • Не можна використовувати оболонки, оскільки труби працюють в різних процесах.
  • Можна використовувати тимчасові файли,
  • або щось на кшталт блокувального файлу або фіфо. Це дозволяє чекати на замок або фіфо,
  • або різних каналів, щоб вивести інформацію, а потім зібрати вихід у певній правильній послідовності.

Наступний шлях може виглядати таким чином (зауважте, що він працює printfостаннім, оскільки це працює тут краще):

_passback() { while [ 0 -lt $# ]; do printf '%q=%q;' "$1" "${!1}"; shift; done; }

_xcapture() { { printf "%q=%q;" "$1" "$("${@:2}" 3<&-; _passback x >&3)"; } 3>&1; }

xcapture() { eval "$(_xcapture "$@")"; }

d() { let x++; date +%Y%m%d-%H%M%S; }

x=0
xcapture d1 d
xcapture d2 d
xcapture d3 d
xcapture d4 d
echo $x $d1 $d2 $d3 $d4

виходи

4 20171129-144845 20171129-144845 20171129-144845 20171129-144845

Чому це правильно?

  • _passback x безпосередньо спілкується з STDOUT.
  • Однак, оскільки STDOUT потрібно зафіксувати у внутрішній команді, ми спочатку "збережемо" його в FD3 (можна використовувати, звичайно, інші) за допомогою "3> & 1", а потім повторно використовуємо >&3.
  • В $("${@:2}" 3<&-; _passback x >&3)обробки після того , як _passback, коли подоболочка закриває STDOUT.
  • Тож printfне може статися раніше _passback, незалежно від того, скільки часу _passbackзаймає
  • Зауважте, що printfкоманда не виконується до монтажу повного командного рядка, тому ми не можемо побачити артефакти з printf, незалежно від того , як printfреалізовано.

Отже спочатку _passbackвиконується, а потім printf.

Це вирішує гонку, жертвуючи одним дескриптором фіксованого файлу 3. Ви, звичайно, можете вибрати інший дескриптор файлу у випадку, якщо FD3 не є вільним у вашому оболонковому сценарії.

Будь ласка, зверніть увагу, 3<&-що захищає FD3 для передачі функції.

Зробіть це більш загальним

_captureмістить частини, які належать d(), що погано, з точки зору повторного використання. Як це вирішити?

Ну, зробіть це відчайдушним шляхом, ввівши ще одну річ, додаткову функцію, яка повинна повертати правильні речі, яка названа на честь оригінальної функції із _доданою.

Ця функція викликається після реальної функції і може доповнювати речі. Таким чином, це можна прочитати як деяку анотацію, тож вона легко читається:

_passback() { while [ 0 -lt $# ]; do printf '%q=%q;' "$1" "${!1}"; shift; done; }
_capture() { { printf "%q=%q;" "$1" "$("${@:2}" 3<&-; "$2_" >&3)"; } 3>&1; }
capture() { eval "$(_capture "$@")"; }

d_() { _passback x; }
d() { let x++; date +%Y%m%d-%H%M%S; }

x=0
capture d1 d
capture d2 d
capture d3 d
capture d4 d
echo $x $d1 $d2 $d3 $d4

досі друкує

4 20171129-151954 20171129-151954 20171129-151954 20171129-151954

Дозволити доступ до коду повернення

Є лише дефіс:

v=$(fn)встановлює $?те, що fnповернулося. Тож ви, мабуть, і цього хочете. Однак це потребує більшого налаштування:

# This is all the interface you need.
# Remember, that this burns FD=3!
_passback() { while [ 1 -lt $# ]; do printf '%q=%q;' "$1" "${!1}"; shift; done; return $1; }
passback() { _passback "$@" "$?"; }
_capture() { { out="$("${@:2}" 3<&-; "$2_" >&3)"; ret=$?; printf "%q=%q;" "$1" "$out"; } 3>&1; echo "(exit $ret)"; }
capture() { eval "$(_capture "$@")"; }

# Here is your function, annotated with which sideffects it has.
fails_() { passback x y; }
fails() { x=$1; y=69; echo FAIL; return 23; }

# And now the code which uses it all
x=0
y=0
capture wtf fails 42
echo $? $x $y $wtf

відбитки

23 42 69 FAIL

Є ще багато можливостей для вдосконалення

  • _passback() можна елімінувати с passback() { set -- "$@" "$?"; while [ 1 -lt $# ]; do printf '%q=%q;' "$1" "${!1}"; shift; done; return $1; }
  • _capture() можна усунути за допомогою capture() { eval "$({ out="$("${@:2}" 3<&-; "$2_" >&3)"; ret=$?; printf "%q=%q;" "$1" "$out"; } 3>&1; echo "(exit $ret)")"; }

  • Розчин забруднює дескриптор файлу (тут 3), використовуючи його внутрішньо. Вам потрібно пам’ятати про це, якщо вам трапляється передавати FD.
    Зауважте, що bash4.1 і вище {fd}повинні використовувати деякі невикористані FD.
    (Можливо, я додам рішення тут, коли я обійдуся.)
    Зауважте, що саме тому я використовую для того, щоб розмістити його в окремих функціях, як-от _capture, оскільки введення цього все в один рядок можливо, але це робить все важче для читання та розуміння

  • Можливо, ви також хочете захопити STDERR викликаної функції. Або ви хочете навіть передавати та передавати більше ніж один fileescriptor із змінних та до них.
    У мене поки що немає рішення, однак тут є спосіб залучити більше ніж один FD , тому ми, ймовірно, можемо також повернути змінні таким чином.

Також не забувайте:

Це повинно викликати функцію оболонки, а не зовнішню команду.

Немає простого способу передачі змінних середовища із зовнішніх команд. ( LD_PRELOAD=Хоча з цим має бути можливо!) Але це тоді щось зовсім інше.

Останні слова

Це не єдине можливе рішення. Це один із прикладів рішення.

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

Представлене тут рішення є далеко не ідеальним:

  • Це майже не було тестуванням, тому, будь ласка, пробачте друкарські помилки.
  • Існує багато можливостей для вдосконалення, див. Вище.
  • Він використовує багато функцій від сучасних bash, тому, ймовірно, важко перенести на інші корпуси.
  • І можуть бути якісь химерності, про які я не думав.

Однак я думаю, що це досить просто у використанні:

  • Додайте лише 4 рядки "бібліотеки".
  • Додайте лише 1 рядок "анотації" для функції оболонки.
  • Жертви лише один дескриптор файлів тимчасово.
  • І кожен крок повинен бути легко зрозуміти навіть через роки.

2
ти приголомшливий
Еліран Малька

14

Можливо, ви можете використовувати файл, записувати у файл всередині функції, читати з файлу після нього. Я змінився eна масив. У цьому прикладі заготовки використовуються як роздільник при зчитуванні назад масиву.

#!/bin/bash

declare -a e
e[0]="first"
e[1]="secondddd"

function test1 () {
 e[2]="third"
 e[1]="second"
 echo "${e[@]}" > /tmp/tempout
 echo hi
}

ret=$(test1)

echo "$ret"

read -r -a e < /tmp/tempout
echo "${e[@]}"
echo "${e[0]}"
echo "${e[1]}"
echo "${e[2]}"

Вихід:

hi
first second third
first
second
third

13

Що ви робите, ви виконуєте тест1

$(test1)

в під оболонці (дочірній оболонці) і дочірні оболонки не можуть нічого змінити в батьківському .

Ви можете знайти його в посібнику з bash

Будь ласка Перевірте: Things результати в субоболочке тут


7

У мене була подібна проблема, коли я хотів автоматично видалити створені мною тимчасові файли. Я придумав рішення не використовувати підстановку команд, а передати ім'я змінної, яка повинна брати кінцевий результат, у функцію. Напр

#! /bin/bash

remove_later=""
new_tmp_file() {
    file=$(mktemp)
    remove_later="$remove_later $file"
    eval $1=$file
}
remove_tmp_files() {
    rm $remove_later
}
trap remove_tmp_files EXIT

new_tmp_file tmpfile1
new_tmp_file tmpfile2

Отже, у вашому випадку це було б:

#!/bin/bash

e=2

function test1() {
  e=4
  eval $1="hello"
}

test1 ret

echo "$ret"
echo "$e"

Працює і не має обмежень на "повернену вартість".


1

Це відбувається тому, що підміна команд виконується в підклітині, тому, поки підзаголовок успадковує змінні, зміни в них втрачаються, коли підзарядка закінчується.

Довідка :

Підстановка команд, команди, згруповані з дужками, і асинхронні команди викликаються в середовищі підклітини, що є дублікатом середовища оболонки


@JohnDoe Я не впевнений, що це можливо. Можливо, вам доведеться переосмислити свій дизайн сценарію.
Якийсь програміст чувак

О, але мені потрібно призначити глобальний масив у межах функції, якщо ні, мені доведеться повторити багато коду (повторити код функції -30 рядків - 15 разів - один за виклик). Іншого шляху немає, чи не так?
harrison4

1

Рішення цієї проблеми, не потребуючи введення складних функцій та сильної модифікації оригіналу, полягає в збереженні значення у тимчасовому файлі та читанні / записуванні його при необхідності.

Такий підхід мені дуже допоміг, коли мені довелося знущатися над функцією bash, яку називали кілька разів у тесті на кажанах.

Наприклад, ви можете мати:

# Usage read_value path_to_tmp_file
function read_value {
  cat "${1}"
}

# Usage: set_value path_to_tmp_file the_value
function set_value {
  echo "${2}" > "${1}"
}
#----

# Original code:

function test1() {
  e=4
  set_value "${tmp_file}" "${e}"
  echo "hello"
}


# Create the temp file
# Note that tmp_file is available in test1 as well
tmp_file=$(mktemp)

# Your logic
e=2
# Store the value
set_value "${tmp_file}" "${e}"

# Run test1
test1

# Read the value modified by test1
e=$(read_value "${tmp_file}")
echo "$e"

Недолік полягає в тому, що вам може знадобитися кілька тимчасових файлів для різних змінних. А також вам може знадобитися syncкоманда для збереження вмісту на диску між операціями запису та читання.


-1

Ви завжди можете використовувати псевдонім:

alias next='printf "blah_%02d" $count;count=$((count+1))'
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.