Как получить последний аргумент функции / bin / sh

11

Какой способ лучше реализовать print_last_arg?

#!/bin/sh

print_last_arg () {
    eval "echo \${$#}"  # this hurts
}

print_last_arg foo bar baz
# baz

(Если бы это было, скажем, #!/usr/bin/zshвместо того, #!/bin/shчтобы знать, что делать. Моя проблема - найти разумный способ реализовать это #!/bin/sh.)

РЕДАКТИРОВАТЬ: выше, это только глупый пример. Моя цель не в том, чтобы напечатать последний аргумент, а в том, чтобы иметь возможность ссылаться на последний аргумент в функции оболочки.


EDIT2: я прошу прощения за такой неясный вопрос. Я надеюсь сделать это правильно на этот раз.

Если бы это было /bin/zshвместо /bin/sh, я мог бы написать что-то вроде этого

#!/bin/zsh

print_last_arg () {
    local last_arg=$argv[$#]
    echo $last_arg
}

Выражение $argv[$#]является примером того, что я описал в своем первом EDIT как способ ссылки на последний аргумент в функции оболочки .

Поэтому я действительно должен был написать свой оригинальный пример так:

print_last_arg () {
    local last_arg=$(eval "echo \${$#}")   # but this hurts even more
    echo $last_arg
}

... чтобы было ясно, что то, что я ищу, - это менее ужасная вещь, которую можно поставить справа от задания.

Однако обратите внимание, что во всех примерах к последнему аргументу обращаются неразрушающим образом . Таким образом, доступ к последнему аргументу оставляет позиционные аргументы в целом без изменений.

KJo
источник
unix.stackexchange.com/q/145522/117549 указывает на различные возможности #! / bin / sh - можете ли вы его сузить?
Джефф Шаллер
мне нравится редактирование, но, возможно, вы, возможно, также заметили, что здесь есть ответ, который предлагает неразрушающий способ ссылки на последний аргумент ...? Вы не должны приравнять var=$( eval echo \${$#})к eval var=\${$#}- два не похожи.
mikeserv
1
Не уверен, что я получил вашу последнюю заметку, но почти все ответы, представленные до сих пор, не являются деструктивными в том смысле, что они сохраняют аргументы запущенного скрипта. Только shiftи set -- ...основанные решения могут быть разрушительными, если они не используются в функциях, где они также безвредны.
Jlliagre
@jlliagre - но они по-прежнему разрушительны в основном - они требуют создания одноразовых контекстов, чтобы они могли разрушать, чтобы обнаружить. но ... если вы все равно получите второй контекст - почему бы просто не получить тот, который позволяет индексировать? Что-то не так с использованием инструмента, предназначенного для работы? интерпретация расширений оболочки как расширяемого ввода - задача eval. и в этом нет ничего особенного eval "var=\${$#}"по сравнению с тем, var=${arr[evaled index]}за исключением того, что $#это гарантированное безопасное значение. зачем копировать весь набор, а затем уничтожать его, когда вы можете просто индексировать его напрямую?
mikeserv
1
@mikeserv Цикл for, выполняемый в основной части оболочки, оставляет все аргументы без изменений. Я согласен, что зацикливание всех аргументов очень неоптимизировано, особенно если тысячи из них передаются в оболочку, и я также согласен с тем, что прямой доступ к последнему аргументу с правильным индексом - лучший ответ (и я не понимаю, почему он был отклонен ) но кроме этого нет ничего действительно разрушительного и никакого дополнительного контекста.
Jlliagre

Ответы:

1
eval printf %s${1+"'\n' \"the last arg is \${$#"\}\"}

... либо напечатает строку, the last arg isза которой следуют <пробел> , значение последнего аргумента и завершающий <newline>, если есть хотя бы 1 аргумент, либо для нулевых аргументов он вообще ничего не напечатает.

Если вы сделали:

eval ${1+"lastarg=\${$#"\}}

... тогда либо вы присвоите значение последнего аргумента переменной оболочки, $lastargесли есть хотя бы один аргумент, либо вообще ничего не сделаете. В любом случае, вы бы сделали это безопасно, и я думаю, что он должен быть переносимым даже для раковины Олд Борн.

Вот еще один, который будет работать аналогичным образом, хотя он требует двойного копирования всего массива arg (и требует использования printfin $PATHдля оболочки Bourne) :

if   [ "${1+:}" ]
then set "$#" "$@" "$@"
     shift    "$1"
     printf %s\\n "$1"
     shift
fi
mikeserv
источник
Комментарии не для расширенного обсуждения; этот разговор был перенесен в чат .
Terdon
2
К вашему сведению, ваше первое предложение не выполняется в устаревшей оболочке Bourne с ошибкой «плохая замена», если нет аргументов. Оба оставшихся работают как положено.
Jlliagre
@jillagre - спасибо. я не был так уверен насчет этого лучшего - но был почти уверен в двух других. это было просто для демонстрации того, как аргументы могут быть доступны через встроенную ссылку. желательно, чтобы функция открывалась с чем-то вроде ${1+":"} returnсразу - потому что, кто хочет, чтобы она что-то делала или рисковала побочными эффектами любого рода, если ее вызывали даром? Математика хороша для того же - если вы можете быть уверены, что вы можете расширить положительное целое число, вы можете evalвесь день. $OPTINDотлично подходит для этого.
mikeserv
3

Вот упрощенный способ:

print_last_arg () {
  if [ "$#" -gt 0 ]
  then
    s=$(( $# - 1 ))
  else
    s=0
  fi
  shift "$s"
  echo "$1"
}

(обновлено на основе замечания @ cuonglm о том, что оригинал не прошел, если не было передано ни одного аргумента; теперь это отображает пустую строку - elseпри желании измените это поведение в предложении)

Джефф Шаллер
источник
Это требовало использования / usr / xpg4 / bin / sh на Solaris; дайте мне знать, если Solaris #! / bin / sh является требованием.
Джефф Шаллер
Этот вопрос не о конкретном сценарии, который я пытаюсь написать, а о том, как это сделать. Я ищу хороший рецепт для этого. Конечно, чем портативнее, тем лучше.
kjo
математика $ (()) не работает в Solaris / bin / sh; используйте что-то вроде: shift `expr $ # - 1`, если это необходимо.
Джефф Шаллер
в качестве альтернативы для Solaris / bin / sh,echo "$# - 1" | bc
Джефф Шаллер
1
это именно то, что я хотел написать, но он потерпел неудачу на 2-й оболочке, которую я пробовал - NetBSD, которая, очевидно, является оболочкой Almquist, которая ее не поддерживает :(
Джефф Шаллер
3

Приведенный пример вступительного поста (позиционные аргументы без пробелов):

print_last_arg foo bar baz

По умолчанию IFS=' \t\n', как насчет:

args="$*" && printf '%s\n' "${args##* }"

Для более безопасного расширения "$*"установите IFS (per @ StéphaneChazelas):

( IFS=' ' args="$*" && printf '%s\n' "${args##* }" )

Но приведенное выше не удастся, если ваши позиционные аргументы могут содержать пробелы. В этом случае используйте это вместо:

for a in "$@"; do : ; done && printf '%s\n' "$a"

Обратите внимание, что эти методы избегают использования evalи не имеют побочных эффектов.

Протестировано на shellcheck.net

AsymLabs
источник
1
Первый сбой, если последний аргумент содержит пробел.
cuonglm
1
Обратите внимание, что первый также не работал в оболочке pre-POSIX
cuonglm
@cuonglm Хорошо заметили, ваше правильное наблюдение теперь включено.
AsymLabs
Также предполагается, что первый символ $IFS- это пробел.
Стефан Шазелас
1
Это будет, если в среде нет $ IFS, не указано иное. Но поскольку вам нужно устанавливать IFS практически каждый раз, когда вы используете оператор split + glob (оставьте расширение без кавычек), один из подходов к работе с IFS - это устанавливать его всякий раз, когда вам это нужно. Было бы не вредно, если бы IFS=' 'здесь было просто дать понять, что оно используется для расширения "$*".
Стефан Шазелас
3

Хотя этому вопросу чуть больше двух лет, я подумал, что поделюсь несколько компактным вариантом.

print_last_arg () {
    echo "${@:${#@}:${#@}}"
}

Давайте запустим это

print_last_arg foo bar baz
baz

Расширение параметров оболочки Bash .

редактировать

Еще более лаконично: echo "${@: -1}"

(Следи за пространством)

Источник

Протестировано на macOS 10.12.6, но также должно возвращать последний аргумент для большинства доступных * nix-вариантов ...

Болит гораздо меньше ¯\_(ツ)_/¯

user2243670
источник
Это должен быть принятый ответ. Еще лучше было бы: на echo "${*: -1}"что shellcheckне будет жаловаться.
Том Хейл,
4
Это не будет работать с простым POSIX sh, ${array:n:m:}это расширение. (вопрос явно упоминается /bin/sh)
ilkkachu
2

POSIXly:

while [ "$#" -gt 1 ]; do
  shift
done

printf '%s\n' "$1"

(Этот подход также работает в старой оболочке Bourne)

С другими стандартными инструментами:

awk 'BEGIN{print ARGV[ARGC-1]}' "$@"

(Это не будет работать со старым awk, которого не было ARGV)

cuonglm
источник
Там, где все еще есть раковина Борна, awkтакже может быть старый awk, которого не было ARGV.
Стефан Шазелас
@ StéphaneChazelas: согласен. По крайней мере, это работало с собственной версией Брайана Кернигана. У тебя есть идеал?
cuonglm
Это стереть аргументы. Как их сохранить?
@BinaryZebra: используйте awkподход, или используйте другой простой forцикл, как другие, или передавая их функции.
cuonglm
2

Это должно работать с любой POSIX-совместимой оболочкой и с прежней POSIX-оболочкой Solaris Bourne:

do=;for do do :;done;printf "%s\n" "$do"

и вот функция, основанная на том же подходе:

print_last_arg()
  if [ "$*" ]; then
    for do do :;done
    printf "%s\n" "$do"
  else
    echo
  fi

PS: не говорите мне, что я забыл фигурные скобки вокруг тела функции ;-)

jlliagre
источник
2
Вы забыли фигурные скобки вокруг тела функции ;-).
@BinaryZebra Я тебя предупреждал ;-) Я их не забыл. Скобки здесь на удивление необязательны.
Jlliagre
1
@jlliagre Я, действительно, был предупрежден ;-): P ...... И: конечно!
Какая часть спецификации синтаксиса позволяет это?
Том Хейл
@TomHale Правила грамматики оболочки допускают как отсутствие in ...в forцикле, так и отсутствие фигурных скобок вокруг ifоператора, используемого в качестве тела функции. Смотрите for_clauseи compound_commandв pubs.opengroup.org/onlinepubs/9699919799 .
Jlliagre
2

Из "Unix - Часто задаваемые вопросы"

(1)

unset last
if    [ $# -gt 0 ]
then  eval last=\${$#}
fi
echo  "$last"

Если количество аргументов может быть равно нулю, тогда $0будет присвоен нулевой аргумент (обычно это имя скрипта) $last. Это причина, если.

(2)

unset last
for   last
do    :
done
echo  "$last"

(3)

for     i
do
        third_last=$second_last
        second_last=$last
        last=$i
done
echo    "$last"

Чтобы избежать вывода пустой строки при отсутствии аргументов, замените echo "$last"for:

${last+false} || echo "${last}"

Нулевой счетчик аргументов избегается if [ $# -gt 0 ].

Это не точная копия того, что находится по ссылке на странице, некоторые улучшения были добавлены.


источник
0

Это должно работать на всех системах с perlустановленным (так большинство UNICES):

print_last_arg () {
    printf '%s\0' "$@" | perl -0ne 's/\0//;$l=$_;}{print "$l\n"'
}

Хитрость заключается в том, чтобы использовать , printfчтобы добавить \0после каждого аргумента оболочки , а затем perl«s -0переключатель , который устанавливает свой разделитель в NULL. Затем мы перебираем ввод, удаляем \0и сохраняем каждую NULL-концевую строку как $l. ENDБлок (это то , что }{есть) будет выполняться после того, как все входные данные были считаны так распечатывает последнюю «линию»: последний аргумент оболочки.

Тердон
источник
@mikeserv ах, правда, я не проверял ни одной строки, кроме последнего аргумента. Отредактированная версия должна работать практически для чего угодно, но, вероятно, слишком сложна для такой простой задачи.
Terdon
да, вызов внешнего исполняемого файла (если он уже не является частью логики) для меня немного сложен. но если смысл в том, чтобы передать этот аргумент sed, awkили perlтогда это может быть ценной информацией в любом случае. Тем не менее, вы можете сделать то же самое с sed -zсовременными версиями GNU.
mikeserv
почему тебя понизили? это было хорошо ... я думал?
mikeserv
0

Вот версия с использованием рекурсии. Не знаю, насколько это соответствует POSIX ...

print_last_arg()
{
    if [ $# -gt 1 ] ; then
        shift
        echo $( print_last_arg "$@" )
    else
        echo "$1"
    fi
}
BRM
источник
0

Простая концепция, без арифметики, без цикла, без оценки , просто функции.
Помните, что у оболочки Борна не было арифметики (нужна внешняя expr). Если вы хотите получить свободный арифметический, evalсвободный выбор, это вариант. Необходимые функции означают SVR3 или выше (без перезаписи параметров).
Посмотрите ниже для более надежной версии с printf.

printlast(){
    shift "$1"
    echo "$1"
}

printlast "$#" "$@"          ### you may use ${1+"$@"} here to allow
                             ### a Bourne empty list of arguments,
                             ### but wait for a correct solution below.

Эта структура вызова printlastявляется фиксированной , аргументы должны быть установлены в списке аргументов оболочки $1, $2и т.д. (стек аргумент) и вызов осуществляется как данность.

Если список аргументов необходимо изменить, просто установите их:

set -- o1e t2o t3r f4u
printlast "$#" "$@"

Или создайте более простую в использовании функцию ( getlast), которая могла бы разрешать общие аргументы (но не так быстро, аргументы передаются два раза).

getlast(){ printlast "$#" "$@"; }
getlast o1e t2o t3r f4u

Обратите внимание, что аргументы ( getlastили все, что включено в $@printlast) могут содержать пробелы, переводы строки и т. Д. Но не NUL.

Лучше

Эта версия не печатает, 0когда список аргументов пуст, и использует более надежный printf (отступите, echoесли внешний printfне доступен для старых оболочек).

printlast(){ shift  "$1"; printf '%s' "$1"; }
getlast  ()  if     [ $# -gt 0 ]
             then   printlast "$#" "$@"
                    echo    ### optional if a trailing newline is wanted.
             fi
### The {} braces were removed on purpose.

getlast 1 2 3 4    # will print 4

Используя EVAL.

Если оболочка Bourne еще старше и нет функций, или если по какой-то причине использование eval является единственным вариантом:

Чтобы напечатать значение:

if    [ $# -gt 0 ]
then  eval printf "'1%s\n'" \"\$\{$#\}\"
fi

Чтобы установить значение в переменной:

if    [ $# -gt 0 ]
then  eval 'last_arg='\"\$\{$#\}\"
fi

Если это нужно сделать в функции, аргументы необходимо скопировать в функцию:

print_last_arg () {
    local last_arg                ### local only works on more modern shells.
    eval 'last_arg='\"\$\{$#\}\"
    echo "last_arg3=$last_arg"
}

print_last_arg "$@"               ### Shell arguments are sent to the function.

источник
это лучше, но он говорит, что не использует цикл - но это делает. копия массива является циклом . и с двумя функциями он использует два цикла. также - есть ли какая-то причина, по которой итерация всего массива должна быть предпочтительнее его индексации eval?
mikeserv
1
Вы видите for loopили while loop?
1
По вашему стандарту, я предполагаю, что весь код имеет циклы, даже циклы a echo "Hello", поскольку ЦП должен считывать ячейки памяти в цикле. Опять же: мой код не имеет добавленных циклов.
1
Мне действительно безразлично ваше мнение о хорошем или плохом, оно уже несколько раз было ошибочным. Опять же : мой код не имеет for, whileили untilпетли. Вы видите какой- for whileлибо untilцикл или цикл, написанный в коде? Нет? Тогда просто примите факт, примите это и учитесь.