Оценить переменную во время объявления функции в оболочке

Я настраиваю свою среду оболочки и хочу иметь возможность использовать некоторые из тех же функций/псевдонимов в zsh, что и в bash. Одна из этих функций открывает .bashrc или .zshrc в редакторе (в зависимости от того, какой файл подходит), ждет закрытия редактора, а затем перезагружает rc-файл.

# a very simplified version of this function
editrc() {
  local rcfile=".$(basename $SHELL)rc"
  code -w ~/$rcfile
  . ~/$rcfile
}

Я использую значение rcfile в нескольких других функциях, поэтому я вытащил его из объявления функции.

_rc=".$(basename $SHELL)rc"
editrc() {
  code -w ~/$_rc
  . ~/$_rc
}

# ... other functions that use it ...
unset _rc

Однако, поскольку я фанат аккуратности, я хочу сбросить _rc в конце своего скрипта, но я все еще хочу, чтобы мои функции работали правильно. Есть ли умный способ оценить $_rc во время объявления функции?

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

_rc=".$(basename $SHELL)rc"
eval 'editrc() {
  echo Here'"'"'s a thing that uses single quotes.  As you can see it'"'"'s a pain.
  code -w ~/'$_rc'
  . ~/'$_rc'
}'
# ... other functions using `_rc`
unset _rc

Я предполагаю, что могу объявить свои функции, а затем поколдовать с eval "$(declare -f editrc | awk)". Очень хорошо, что это принесет больше боли, чем пользы, но мне всегда интересно узнавать что-то новое.

Примечание. Я хотел бы обобщить это в функцию полезности, которая делает это.

_myvar=foo
anothervar=bar
myfunc() {
  echo $_myvar $anothervar
}

# redeclares myfunc with `$_myvar` expanded, but leaves `$anothervar` as-is
expandfunctionvars myfunc '$_myvar' 

person dx_over_dt    schedule 02.08.2020    source источник
comment
Ваша функция не будет работать надежно, потому что если вы запустите bash и изнутри откроете zsh как подоболочку, SHELL все равно будет установлено значение bash. Обходным путем было бы поместить export SHELL=/bin/zsh в ~/.zshenv.   -  person user1934428    schedule 04.08.2020
comment
@user1934428 user1934428 Полагаю, мне нужно сделать то же самое для bash в zsh? Я все еще новичок в обеих оболочках. Что такое .zshenv эквивалент bash?   -  person dx_over_dt    schedule 04.08.2020
comment
Я не знаю, есть ли точный аналог. Однако bash автоматически устанавливает переменную SHELL, если она еще не установлена, и вам не нужно заботиться о оболочках входа в систему bash. Для интерактивной подоболочки bash, запущенной из zsh, вы можете поместить определение в .bashrc. Для неинтерактивного bash вам не повезло, но если вы пишете сценарий, вы знаете, какой язык используется, и можете явно установить переменную в сценарии.   -  person user1934428    schedule 05.08.2020


Ответы (2)


Есть ли умный способ оценить $_rc во время объявления функции?

_rc=".$(basename "$SHELL")rc"
# while you could eval here, source lets you work with a stream
source <(
  cat <<EOF
  editrc() {
       local _rc
       # first safely trasfer context
       $(declare -p _rc)
EOF
  # use quoted here string to do anything inside without caring. 
  cat <<'EOF' 
     # do anything else
     echo "Here's a thing that uses single quotes.  As you can see it's not a pain, just choose proper quoting."
      code -w "~/$_rc"
      . "~/$_rc"
  }
EOF
)
unset _rc

Обычно сначала используйте declare -p для передачи переменных в виде строк для оценки. Затем, после того, как вы импортируете переменные, используйте цитируемый здесь документ, чтобы делать что-либо, как в обычном скрипте.

Ссылки для чтения:

  • <<EOF — это здесь документ. Обратите внимание на разницу в синтаксическом анализе, когда разделитель здесь заключен в кавычки, а не в кавычки.
  • <(..) — это замена процесса

Команда source читает канал, созданный заменой процесса. Внутри подстановки процесса я вывожу функцию, которую нужно получить. В первом здесь документе я вывожу определение имени функции с local переменной, чтобы оно не загрязняло глобальное пространство имен. Затем с помощью declare -p я вывожу определение переменной как правильно заключенную в кавычки строку, которая позже будет получена source. Затем с цитируемым здесь документом я вывожу остальную часть функции, так что мне не нужно заботиться о цитировании.

Код специфичен для bash, я ничего не знаю о zsh и не использую его.

Вы также можете сделать это с помощью eval:

eval '
editrc() {
       local _rc
       # first safely trasfer context
       '"$(declare -p _rc)"'
       # use quoted here string to do anything inside without caring. 
       # do anything else
       echo "Here'\''s a thing that uses single quotes.  As you can see it'\''s not a pain, just choose proper quoting."
      code -w "~/$_rc"
      . "~/$_rc"
}'

Но для меня использование цитируемого здесь разделителя документов упрощает написание.

person KamilCuk    schedule 02.08.2020
comment
Не могли бы вы объяснить это? Похоже, вы создаете список и помещаете его на вход источника (хотя я не знаю разницы между < и <<. Я также не знаю, что делает cat <<EOF. И можно ли это решение обобщить? - person dx_over_dt; 03.08.2020
comment
С большей вариативностью просто сделайте local var1 var2 var3 ... и сделайте declare -p var1 var2 var3.... У меня есть сценарий, в котором я использую этот метод в дикой природе здесь< /а>. - person KamilCuk; 03.08.2020
comment
Я только что опубликовал то, что я имел в виду под обобщенным решением. - person dx_over_dt; 03.08.2020

Пока KamilCuck работал над их ответом, я разработал функцию, которая будет принимать любое имя функции и набор имен переменных, расширять только эти переменные и повторно объявлять функцию.

expandFnVars() {
  if [[ $# -lt 2 ]]; then
    >&2 echo 'expandFnVars requires at least two arguments: the function name and the variable(s) to be expanded'
    return 1
  fi

  local fn="$1"
  shift

  local vars=("$@")

  if [[ -z "$(declare -F $fn 2> /dev/null)" ]]; then
    >&2 echo $fn is not a function.
    return 1
  fi

  foundAllVars=true
  for v in $vars; do
    if [[ -z "$(declare -p $v 2> /dev/null)" ]]; then
      >&2 echo $v is not a declared value.
      foundAllVars=false
    fi
  done

  [[ $foundAllVars != true ]] && return 1

  fn="$(declare -f $fn)"

  for v in $vars; do
    local val="$(eval 'echo $'$v)" # get the value of the varable represented by $v
    val="${val//\"/\\\"}" # escape any double-quotes
    val="${val//\\/\\\\\\}" # escape any backslashes
    fn="$(echo "$fn" | sed -r 's/"?\$'$v'"?/"'"$val"'"/g')" # replace instances of "$$v" and $$v with $val
  done

  eval "$fn"
}

Применение:

foo="foo bar"
bar='$foo'
baz=baz

fn() {
  echo $bar $baz
}

expandFnVars fn bar

declare -f fn
# prints:
#  fn () 
#  {
#     echo "$foo" $baz
#  }

expandFnVars fn foo

declare -f fn
# prints:
#  fn () 
#  {
#     echo "foo bar" $baz
#  }

Глядя на него сейчас, я вижу один недостаток. Предположим, что $bar в исходной функции было заключено в одинарные кавычки. Мы, вероятно, не хотели бы, чтобы его значение было заменено. Это может быть исправлено с помощью некоторых умных регулярных выражений для подсчета количества неэкранированных ', но я доволен этим как есть.

person dx_over_dt    schedule 02.08.2020
comment
С ним есть и другие проблемы: например, если вы используете его для замены $foo (который содержит bar), а функция содержит echo $food, она изменит его на echo bard. Если значение одной переменной содержит имя другой переменной, оно будет заменено ее значением (в противном случае этого бы не произошло). Если функция содержит echo "value: $foo", вторая " будет удалена, что вызовет синтаксическую ошибку. Он не заменит ${foo}, что заменит оболочка. (Некоторые из этих проблем могут быть легко устранены, другие — менее.) - person psmears; 03.08.2020
comment
Хорошие моменты. Хотите попробовать лучшее решение? - person dx_over_dt; 03.08.2020