Приріст лічильника в циклі Баша не працює


125

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

#!/bin/bash

WFY_PATH=/var/log/nginx
WFY_FILE=error.log
COUNTER=0
grep 'GET /log_' $WFY_PATH/$WFY_FILE | grep 'upstream timed out' | awk -F ', ' '{print $2,$4,$0}' | awk '{print "http://domain.com"$5"&ip="$2"&date="$7"&time="$8"&end=1"}' | awk -F '&end=1' '{print $1"&end=1"}' |
(
while read WFY_URL
do
    echo $WFY_URL #Some more action
    COUNTER=$((COUNTER+1))
done
)

echo $COUNTER # output = 0


Вам не потрібно вводити цикл у підзарядку. Просто зніміть дужки навколо циклу, досить. Інакше, якщо ви повинні поставити його цикл у підрозділ, після цього, поки це зробите, скиньте лічильник у тимчасовий файл один раз і відновіть цей файл поза підшлухом. Я підготую вам остаточну процедуру у відповідь.
Znik

Відповіді:


156

По-перше, ви не збільшуєте лічильник. Зміна COUNTER=$((COUNTER))в COUNTER=$((COUNTER + 1))або COUNTER=$[COUNTER + 1]збільшить його.

По-друге, складніше резервувати змінні підзарядних частин для виклику, коли ви здогадаєтесь. Змінні в підскладі недоступні за межами підклітини. Це змінні, локальні для дочірнього процесу.

Один із способів вирішити це - використання тимчасового файлу для зберігання проміжного значення:

TEMPFILE=/tmp/$$.tmp
echo 0 > $TEMPFILE

# Loop goes here
  # Fetch the value and increase it
  COUNTER=$[$(cat $TEMPFILE) + 1]

  # Store the new value
  echo $COUNTER > $TEMPFILE

# Loop done, script done, delete the file
unlink $TEMPFILE

30
$ [...] застаріло.
чепнер

1
@chepner Чи є у вас посилання, яке говорить про $[...]застаріле? Чи є альтернативне рішення?
вибухнув

9
$[...]використовувалася bashперш , ніж $((...))був прийнятий POSIX оболонки. Я не впевнений, що він коли-небудь формально застарів, але я не можу знайти його згадки на bashсторінці "man", і, здається, він підтримується лише для зворотної сумісності.
чепнер

Також перевага віддається $ (...)...
Lennart Rolland,

7
@blong Ось так питання на $ [...] проти $ ((...)) , що обговорює і посилання старіння: stackoverflow.com/questions/2415724 / ...
Ogre Psalm33

87
COUNTER=1
while [ Your != "done" ]
do
     echo " $COUNTER "
     COUNTER=$[$COUNTER +1]
done

ТЕСТОВАНИЙ БАШ: Centos, SuSE, RH


1
@kroonwijk має бути пробіл перед квадратною дужкою (щоб "розмежувати слова", формально кажучи). Bash інакше не може побачити кінець попереднього виразу.
EdwardGarson

1
питання було певний час з трубою, тож там, де створена
нижня оболонка

2
Відповідно до коментаря Чепнера щодо іншої відповіді, $[ ]синтаксис застарілий. stackoverflow.com/questions/10515964 / ...
Відзначити Haferkamp

це не вирішує головне питання, головна петля розміщена під
підзарядкою

42
COUNTER=$((COUNTER+1)) 

є досить незграбною конструкцією в сучасному програмуванні.

(( COUNTER++ ))

виглядає більш «сучасно». Ви також можете використовувати

let COUNTER++

якщо ви думаєте, що це покращує читабельність. Іноді Баш дає занадто багато способів робити речі - я вважаю, філософія Перла - коли, можливо, Пітоном "є лише один правильний спосіб зробити це" може бути більш підходящим. Це суперечливе твердження, якщо колись таке було! У будь-якому разі, я б запропонував, що мета (в даному випадку) не просто збільшити змінну, але (загальне правило) також написати код, який хтось інший може зрозуміти та підтримати. Відповідність проходить довгий шлях до досягнення цього.

HTH


Це не стосується початкового запитання, яке полягає в тому, як отримати оновлене значення у лічильнику ПІСЛЯ закінчення циклу (підпроцесу)
Луїс Васкес

16

Спробуйте використовувати

COUNTER=$((COUNTER+1))

замість

COUNTER=$((COUNTER))

8
або простоlet "COUNTER++"
недійсний

2
Вибачте, це був друкарський помилок. Її насправді ((КРАЙНА + 1))
Спарш Гупта

8
@AaronDigulla: (( COUNTER++ ))(без знака долара)
Призупинено до подальшого повідомлення.

2
Я не впевнений, чому, але я бачу, що мій сценарій неодноразово виходить з ладу при використанні, (( COUNTER++ ))але коли я перейшов на COUNTER=$((COUNTER + 1))нього, працював. GNU bash, version 4.1.2(1)-release (x86_64-redhat-linux-gnu)
Стівен Лу

Можливо, ваша лінія хеш-бангу виконує bash як / bin / sh замість / bin / bash?
Макс

12

Я думаю, що цей єдиний виклик awk еквівалентний вашому grep|grep|awk|awkтрубопроводу: будь ласка, протестуйте його. Здається, що ваша остання команда awk взагалі нічого не змінює.

Проблема COUNTER полягає в тому, що цикл while працює в підшалі, тому будь-які зміни змінної зникають при виході нижньої частини. Вам потрібно отримати доступ до значення COUNTER у тій же підпакеті. Або скористайтеся порадою @ DennisWilliamson, використовуйте процедуру заміни і взагалі уникайте передплати.

awk '
  /GET \/log_/ && /upstream timed out/ {
    split($0, a, ", ")
    split(a[2] FS a[4] FS $0, b)
    print "http://example.com" b[5] "&ip=" b[2] "&date=" b[7] "&time=" b[8] "&end=1"
  }
' | {
    while read WFY_URL
    do
        echo $WFY_URL #Some more action
        (( COUNTER++ ))
    done
    echo $COUNTER
}

1
Дякую, останній awk в основному видалить все після закінчення = 1 і поставить новий кінець = 1 до кінця (так що наступного разу ми зможемо видалити все, що додається після нього).
Sparsh Gupta

1
@SparshGupta, попередній awk нічого не друкує після "end = 1".
Глен Джекман

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


11

Замість використання тимчасового файлу ви можете уникнути створення підшаровок навколо whileциклу, використовуючи підстановку процесу.

while ...
do
   ...
done < <(grep ...)

До речі, ви повинні мати можливість перетворити все це grep, grep, awk, awk, awkв єдине ціле awk.

Починаючи з Bash 4.2, є lastpipeваріант, який

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

bash -c 'echo foo | while read -r s; do c=3; done; echo "$c"'

bash -c 'shopt -s lastpipe; echo foo | while read -r s; do c=3; done; echo "$c"'
3

Підміна процесу відмінно підходить, якщо ви хочете збільшити лічильник всередині циклу і використовувати його зовні, коли це зроблено, проблема з підмінами процесу полягає в тому, що я не знайшов способу отримати код статусу виконаної команди, який можливий при використанні труби використовуючи $ {PIPESTATUS [*]}
chrisweb

@chrisweb: я додав інформацію про lastpipe. До речі, вам, мабуть, слід скористатися "${PIPESTATUS[@]}"(at замість зірочки).
Призупинено до подальшого повідомлення.

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

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


3

Це все, що вам потрібно зробити:

$((COUNTER++))

Ось уривок із « Навчання баш-шеллу» , 3-е видання, стор. 147, 148:

арифметичні вирази bash еквівалентні їх аналогам у мовах Java та C. [9] Пріоритетність та асоціативність такі ж, як у C. У таблиці 6-2 показані підтримувані арифметичні оператори. Хоча деякі з них є (або містять) спеціальні символи, немає необхідності відхиляти їх від нахилу, оскільки вони знаходяться в синтаксисі $ ((...)).

..........................

Оператори ++ і - корисні, коли ви хочете збільшити або зменшити значення на одиницю. [11] Вони працюють так само, як у Java та C, наприклад, значення ++ збільшує значення на 1. Це називається пост-збільшенням ; також існує попереднє збільшення : значення ++ . Різниця стає очевидною на прикладі:

$ i=0
$ echo $i
0
$ echo $((i++))
0
$ echo $i
1
$ echo $((++i))
2
$ echo $i
2

Дивіться http://www.safaribooksonline.com/a/learning-the-bash/7572399/


Ця версія мені потрібна, тому що я використовував її за умови ifзаяви: if [[ $((needsComma++)) -gt 0 ]]; then printf ',\n'; fi Правильно чи неправильно, це єдина версія, яка надійно працювала.
LS

Що важливо в цій формі, це те, що ви можете використовувати приріст в один крок. i=1; while true; do echo $((i++)); sleep .1; done
Бруно Броноський

1
@LS: if (( needsComma++ > 0 )); thenабоif (( needsComma++ )); then
Призупинено до подальшого повідомлення.

Використовуючи "echo $ ((i ++))" у bash, я завжди отримую "/opt/xyz/init.sh: рядок 29: команда не знайдено" Що я роблю неправильно?
mmo

Це не стосується питання про отримання значення лічильника поза циклом.
Луїс Васкес

1

Це простий приклад

COUNTER=1
for i in {1..5}
do   
   echo $COUNTER;
   //echo "Welcome $i times"
   ((COUNTER++));    
done

1
простий приклад, але не піддається сумніву.
Зник

0

Здається, ви не оновили counterсценарій, використовуйтеcounter++


Вибачте за друкарський помилок, я фактично використовую ((КРАЙНА + 1)) у сценарії, який не працює
Sparsh Gupta

не важливо, чи вона збільшується за значенням + 1, або за значенням ++. Після закінчення додаткової оболонки лічильник втрачається і повертається до початкового значення 0, встановленого при запуску цього сценарію.
Znik

0

Було дві умови, які спричинили ((var++))невдачу виразу для мене:

  1. Якщо я встановив bash на суворий режим ( set -euo pipefail) і якщо я запускаю приріст у нуль (0).

  2. Починати з одиниці (1) добре, але нуль викликає приріст повернення "1" при оцінці "++", що є ненульовим збоєм повернення коду в суворому режимі.

Я можу використовувати ((var+=1))або var=$((var+1))уникати такої поведінки


0

У вихідного сценарію є певна проблема з підзарядкою. Перший приклад, вам, ймовірно, не потрібна підкатегорія. Але ми не знаємо, що приховано під "Ще деякими діями". Найпопулярніша відповідь - це прихована помилка, яка збільшить введення / виведення, і не працюватиме з підзарядкою, оскільки вона відновлює куртер всередині циклу.

Не забороняйте додавати знак "\", він повідомить інтерпретатора bash про продовження рядка. Я сподіваюся, що це допоможе тобі чи комусь. Але, на мою думку, цей сценарій слід повністю перетворити на сценарій AWK, або ж переписати на python за допомогою regexp або perl, але популярність Perl протягом багатьох років принижується. Краще робити це з пітоном.

Виправлена ​​версія без додаткової оболонки:

#!/bin/bash
WFY_PATH=/var/log/nginx
WFY_FILE=error.log
COUNTER=0
grep 'GET /log_' $WFY_PATH/$WFY_FILE | grep 'upstream timed out' |\
awk -F ', ' '{print $2,$4,$0}' |\
awk '{print "http://example.com"$5"&ip="$2"&date="$7"&time="$8"&end=1"}' |\
awk -F '&end=1' '{print $1"&end=1"}' |\
#(  #unneeded bracket
while read WFY_URL
do
    echo $WFY_URL #Some more action
    COUNTER=$((COUNTER+1))
done
# ) unneeded bracket

echo $COUNTER # output = 0

Версія з допоміжною оболонкою, якщо вона дійсно потрібна

#!/bin/bash

TEMPFILE=/tmp/$$.tmp  #I've got it from the most popular answer
WFY_PATH=/var/log/nginx
WFY_FILE=error.log
COUNTER=0
grep 'GET /log_' $WFY_PATH/$WFY_FILE | grep 'upstream timed out' |\
awk -F ', ' '{print $2,$4,$0}' |\
awk '{print "http://example.com"$5"&ip="$2"&date="$7"&time="$8"&end=1"}' |\
awk -F '&end=1' '{print $1"&end=1"}' |\
(
while read WFY_URL
do
    echo $WFY_URL #Some more action
    COUNTER=$((COUNTER+1))
done
echo $COUNTER > $TEMPFILE  #store counter only once, do it after loop, you will save I/O
)

COUNTER=$(cat $TEMPFILE)  #restore counter
unlink $TEMPFILE
echo $COUNTER # output = 0
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.