Як отримати останній аргумент до функції / 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.)

EDIT: Наведене - лише дурний приклад. Моя мета - не надрукувати останній аргумент, а скоріше мати спосіб посилання на останній аргумент у функції оболонки.


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
}

... щоб зрозуміти, що те, що я переживаю, - це менш жахлива річ, яку слід ставити праворуч від завдання.

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


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 Для циклу, виконаного в основній частині оболонки, всі аргументи залишаються незмінними. Я погоджуюсь, що циклічність усіх аргументів дуже неоптимізована, особливо якщо тисячі з них передаються в оболонку, і я також погоджуюся, що найкращий варіант відповіді на останній аргумент з відповідним індексом (і я не розумію, чому це було спровоковано ) але крім цього, нічого насправді не руйнує і не створюється зайвий контекст.
jlliagre

Відповіді:


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

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

Якщо ви зробили:

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

... тоді ви або призначили б значення останнього аргументу змінній оболонки, $lastargякщо є принаймні 1 аргумент, або ви взагалі нічого не зробите. Так чи інакше, ви б це зробили благополучно, і це повинно бути портативним навіть для оболонки Олд Борна, я думаю.

Ось ще один, який би працював аналогічно, хоча для цього потрібно копіювати весь масив аргументів двічі (і потрібен printfв $PATHдля оболонки Bourne) :

if   [ "${1+:}" ]
then set "$#" "$@" "$@"
     shift    "$1"
     printf %s\\n "$1"
     shift
fi

Коментарі не для розширеного обговорення; ця розмова була переміщена до чату .
тердон

2
FYI, Ваша перша пропозиція не відповідає старій оболонці 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
Jeff Schaller

1
це саме те, що я хотів написати, але це не вдалося на 2-й оболонці, яку я спробував - NetBSD, що, мабуть, є алквістською оболонкою, яка не підтримує її :(
Джефф Шалер

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


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

1
Зауважте, що перший також не працював у оболонці перед POSIX
cuonglm

@cuonglm Добре помічено, ваше правильне спостереження включено зараз.
AsymLabs

Він також передбачає, що першим символом $IFSє пробіл.
Стефан Шазелас

1
Це буде, якщо в середовищі немає $ IFS, не визначено інше. Але оскільки вам потрібно встановити IFS практично кожен раз, коли ви використовуєте оператор split + glob (залиште розширення без котирування), одним із підходів до роботи з IFS є встановлення його завжди, коли вам це потрібно. Не зашкодить існувати IFS=' 'тут просто, щоб зрозуміти, що він використовується для розширення "$*".
Стефан Шазелас

3

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

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

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

print_last_arg foo bar baz
baz

Розширення параметра оболонки Bash .

Редагувати

Ще більш стисло: echo "${@: -1}"

(Пам'ятайте про простір)

Джерело

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

Шкодить набагато менше ¯\_(ツ)_/¯


Це має бути прийнятою відповіддю. Ще краще було б: на echo "${*: -1}"що shellcheckне будеш скаржитися.
Том Хейл

4
Це не працюватиме з звичайним POSIX sh, ${array:n:m:}є розширенням. (питання чітко згадувалося /bin/sh)
ilkkachu

2

POSIXly:

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

printf '%s\n' "$1"

(Цей підхід також працює у старій оболонці Борна)

З іншими стандартними інструментами:

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

(Це не працюватиме зі старими awk, яких не було ARGV)


Там, де ще є оболонка Борна, awkтакож міг бути старий awk, якого не було ARGV.
Стефан Шазелас

@ StéphaneChazelas: Погодьтеся. Принаймні, це працювало з власною версією Брайана Керніган. У вас є ідеал?
cuonglm

Це стирає аргументи. Як їх утримувати ?.

@BinaryZebra: Використовуйте awkпідхід або використовуйте інший простий forцикл, як це робив, або передаючи їх функції.
cuonglm

2

Це повинно працювати з будь-якою оболонкою, сумісною з 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: не кажіть мені, що я забув фігурні брекети навколо корпусу функції ;-)


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":

${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 ах, правда, я не перевіряв новий рядок, окрім останнього аргументу. Відредагована версія повинна працювати майже нічого, але, ймовірно, занадто складна для такого простого завдання.
тердон

так, виклик зовнішнього виконуваного файлу (якщо це вже не є логікою) , для мене, трохи важкий. але якщо точка передати цей аргумент на 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
}

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цикл, записаний у коді ?. Ні ?, тоді просто прийняти факт, прийняти його та дізнатися.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.