Я неправильно использую цикл while?

Я нашел список проектов, и один из них был чем-то, что генерирует количество изменений. Я сделал этот код:

getamt() { echo "Enter amount of money." read amount echo "OK." } change() { amount=$(echo "$amount*100" | bc) quarter=$(echo "($amount-25)" | bc) dime=$(echo "($amount-10)" | bc) nickel=$(echo "($amount-5)" | bc) penny=$(echo "($amount-1)" | bc ) quarter=${quarter%???} dime=${dime%???} nickel=${nickel%???} penny=${penny%???} amount=${amount%???} qNum=0 dNum=0 nNum=0 pNum=0 } getchange() { while [ $quarter -ge 0 ] do qNum=$(( qNum+1 )) amount=$(( $amount-25 )) done while [ $dime -ge 0 ] do dNum=$(( dNum+1 )) amount=$(( $amount-10 )) done while [ $nickel -ge 0 ] do nNum=$(( nNum+1 )) amount=$(( $amount-5 )) done while [ $penny -ge 0 ] do pNum=$(( nNum+1 )) amount=$(( $amount-1 )) done } display() { echo "Your change is:" echo "$qNum quarters" echo "$dNum dimes" echo "$nNum nickels" echo "$pNum pennies" } getamt change getchange display 

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

Наиболее очевидная проблема вашего кода заключается в том, что все ваши циклы while проверяют переменную (например, $quarter ), которая никогда не изменяется внутри цикла, поэтому условие цикла никогда не может стать ложным и цикл повторяется бесконечно.

Давайте посмотрим на один из циклов:

 while [ $quarter -ge 0 ] do qNum=$(( qNum+1 )) amount=$(( $amount-25 )) done 

Если $quarter > 0, поток управления вступает в цикл, $qNum увеличивается и $amount уменьшается, но $quarter остается неизменной, поэтому вы находитесь в другой итерации цикла.


Исправление вашего кода лучше всего работает, реорганизовывая его:

  • Вместо того, чтобы полагаться на глобальные переменные, такие как amount , заданное как побочные эффекты функций, переписывайте свои функции, чтобы принимать параметры и выводить их результаты в stdout (где это возможно).

    • Результаты для stdout : Ваша функция getamt() могла бы echo $amount вместо того, чтобы полагаться на amount , доступное (и неизменное) для последующей обработки в скрипте. Независимо от того, getamt вызовы getamt может затем getamt этот вывод в переменную с amount=$(getamt) .
      К сожалению, это не работает, когда функция должна возвращать несколько значений – в этом случае вы можете заставить функцию печатать свои возвращаемые значения, разделенные символами новой строки, или символ, который, как вы знаете, не будет отображаться в значениях. Вы даже можете выбрать выходной формат, например

       quarter=3 dime=1 nickel=4 

      и оценивать этот вывод для установки локальных переменных с возвращаемыми значениями функции: $(yourfunction); echo $quarter $(yourfunction); echo $quarter

    • Параметры: Ваша функция change() может принимать величину изменения, которую он должен вычислить как параметр (т. Е. Вы бы набрали amount 2.50 ) вместо того, чтобы читать ее из глобальной переменной. Вы можете получить доступ к параметрам, предоставленным вашей функции (или вашему скрипту, в зависимости от контекста) через их индексы: $1 для первого параметра, $2 для второго и т. Д.

  • Вы можете избежать нескольких вызовов на bc , просто отключив десятичные разряды один раз и используя только арифметическую оценку bash после этого. Ваша текущая подстановка ${quarter%???} также удаляет любые последние три символа, что приведет к нежелательным результатам, если ваши пользователи когда-либо решат ввести значение больше (или меньше), чем два десятичных знака. Используйте что-то вроде ${quarter%%.*} Чтобы удалить все после (и включая) первое . ,

  • Используйте комментарии (начатые с символа # и продолжающиеся до конца строки):
    например amount=${amount%%.*} # remove decimal places
    Большая часть вашего кода будет казаться вам очевидной прямо сейчас, но это может быть неочевидно для кого-либо еще, глядя на нее, и это также не будет очевидно для вас, когда вам придется снова взглянуть на нее через несколько месяцев ,

  • Честно говоря, я не совсем уверен, как ваш сценарий должен рассчитать количество монет, которые нужно вернуть в данный момент. Наиболее распространенным подходом к вычислению изменений будет жадный алгоритм, который начинается с наивысшего доступного значения монет, распределяет так много монет этого значения, как «подгонку» в сумму изменения 1 , вычитает общее значение этих монет из суммы изменения, затем продолжается со следующего (меньшего) значения монеты и т. д. до тех пор, пока сумма изменений не достигнет 0 (т. е. было выбрано достаточно монеты, чтобы составить общую сумму изменения).
    1 Чтобы вычислить это количество монет, вы можете либо посмотреть на операции по модулю, либо просто вычесть текущее значение монеты из суммы изменения в цикле до тех пор, пока сумма изменения не станет меньше значения монеты (т. Е. Вы получите слишком много изменений, если вы выдадите другая монета текущего значения).

Из двух функций оболочки ниже, фактическая математика только что сделана здесь:

 while set "${1#0?}" "${1#?}" shift "$((!${#1}))" [ "${1:-0}" -gt 0 ] do case $1 in ([3-9]?|2[5-9]) set "$(($1%25))" "$((q+=$1/25))";; (??) set "$(($1%10))" "$((d=$1/10))" ;; (?) set "" "$((p=$1-(5*(n=$1>=5))))";; esac; done 

Это весь код выбора монет – он оптимизирован для возврата как можно большего количества монет. И мне не нужно было ничего делать, потому что так работает оператор управления оболочкой корпуса – выбирая только самое раннее возможное совпадение. Итак, все, что необходимо, – это поместить монеты в порядок от самых маленьких до наименьших, и вы никогда не превысите 3 итерации.

Единственная трудная часть из вышеперечисленного – защита переносимой математики оболочки от неправильного толкования результатов как восьмеричного значения в случае 08 и 09. Это обрабатывается путем сжимания всех ведущих нулей после каждого цикла.

На самом деле, учитывая, что ваша заявленная цель состоит в том, чтобы вводить и предоставлять результаты интерактивному пользователю, большинство нижеприведенных функций сосредоточены главным образом на проверке ввода и представлении ошибок. Это тоже важный материал, особенно в том, что касается математики оболочки. Поскольку математика оболочки – это, по сути, 2-компонентная операция eval , когда вы вводите ввод пользователя в арифметическую инструкцию, вы, вероятно, сначала убедитесь, что знаете, что у вас там есть.

case , опять же, моя форма goto для таких вещей.

 _err()( unset parm msg IFS \ "${1##*[!_[:alnum:]]*}" || exit parm=$1 IFS=$2 msg=$3; shift 3 eval ': "${'"$parm?\"'\$*' can't be right. \$msg"'"}"' ) _chg() if set -- "${1#"${1%%[!0]*}"}.${2%"${2#??}"}${3+.}" "$@" && case $1 in (*.*.*) shift _err too_many_dots . " We're fresh out of microcoins." "$@" ;; (-*) shift _err nice_try_pal . " Change isn't magic money, you know." "$@" ;; (*[!0-9.]*) shift _err i_hate_poetry . " We only spend numbers around here." "$@" ;; (.00|.0|.) shift _err that_was_easy . " Next time try spending something." 0 00 ;; esac || return then set "${1##*.}" "$((q=(${1%%.*}0*4)/10+(d=(n=(p=0)))))" while set "${1#0?}" "${1#?}" shift "$((!${#1}))" [ "${1:-0}" -gt 0 ] do case $1 in ([3-9]?|2[5-9]) set "$(($1%25))" "$((q+=$1/25))";; (??) set "$(($1%10))" "$((d=$1/10))" ;; (?) set "" "$((p=$1-(5*(n=$1>=5))))";; esac; done set quarter q dime d nickel n penny p echo Your change is: while [ "$#" -gt 1 ] do printf "\t$1 coins:\t$(($2))\n" shift 2 done; fi 

Однако на самом деле он не read никаких данных и принимает только входные параметры в качестве аргументов командной строки. Вы можете тянуть пользовательский ввод с чем-то вроде:

 printf '\n$ '; IFS=. read -r dollars cents dot 

И прямо передайте это, как …

 _chg "$dollars" "$cents" ${dot:+""} 

… и все остальное должно быть автоматическим.

Функция _err() – это многократно используемая функция, которую я написал, которую вы можете использовать здесь или в другом месте для сообщения об ошибках с правильным возвратом. Когда вы разворачиваете unset ${var?expansion form} Оболочка будет печатать форму расширения в stderr и резко выходить со статусом ошибки. Это не поведение, которое обычно хорошо работает для тестов, которые вы, возможно, хотели бы обрабатывать самостоятельно, но если вы знаете, что какое-то условие, которое должно выполняться для того, чтобы этот параметр unset вообще расширялся, определенно является условием, которое означает ваш процесс должен умереть, тогда это может быть очень удобным способом. Это связано с тем, что оболочка форматирует весь вывод своим собственным стандартным способом (к которому, вероятно, уже привык пользователь интерактивной оболочки) , и обрабатывает ваш код выхода сразу.

Например:

 bash -c '. ~/coins.sh _err parameter_name \ -splitter \ "Some custom message that is also thrown in." \ and my entire input arg array ' 

… при запуске в командной строке возвращает 1 и печатает в stderr …

 /home/mikeserv/coins.sh: line 5: parameter_name: 'and-my-entire-input-arg-array' can't be right. Some custom message that is also thrown in. 

И поэтому вся верхняя половина _chg() посвящена проверке ввода – это то, что должно быть или возвращать условие ошибки и вывод ошибки, когда это не так.

Последняя четверть того же посвящена форматированию stdout, когда все идет хорошо:

 sh -c '. ~/coins.sh; _chg 10 97' Your change is: quarter coins: 43 dime coins: 2 nickel coins: 0 penny coins: 2 

Другой ответ затронул вашу конкретную проблему. Некоторое время я несколько раз пытался разобраться. Итак, вот еще один подход, который вы можете рассмотреть – с одним while и одним, а затем и одним for цикла. Массивы помогают упростить код.

  echo "Enter amount of money: $.c or just $" read amount echo a=(${amount/./ }) # change '.' to ' ' and make an array: a[0], a[1] da=${a[0]} # dollar-amount pa=$((10#${a[1]})) # penny-amount cv=(25 10 5 1) # array of coin-values cv[0] ... cv[3] - qdnp cc=(\ \ \ \ ) # array of coin-counts cc[0] ... cc[3] - qdnp cn=( quarters dimes nickels pennies ) # array of coin-names while (( pa > 0 )); do for (( i=0; i<${#cv[@]}; i++ )); do # process coin-types from hi-val to lo-val (( (pa-cv[i]) < 0 )) && continue # look-ahead: don't give too much change (( (pa-=cv[i]) )) # decrement penny-amount (( cc[i]+=1 )) # increment coin-type counters done done # 'paste' arrrays side by side, and tabulate via 'column' echo "Your coins change is:" # and show only relevant coins via 'sed' column -t <(paste <(printf '%s\n' "${cn[@]}") \ <(printf '%s\n' "${cc[@]}")) | sed -n '/[0-9]/p'