Приращение счетчика в цикле Bash не работает


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


Вам не нужно помещать цикл while в подоболочку. Просто снимите скобки вокруг цикла while, этого достаточно. Или, если вы должны поместить его в подоболочку, то после того, как вы сделаете это, сделайте дамп счетчика во временный файл один раз и восстановите этот файл вне подоболочки. В ответ я подготовлю вам финальную процедуру.
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
$ [...] устарел.
chepner

1
@chepner У вас есть ссылка, в которой говорится, что $[...]она устарела? Есть ли альтернативное решение?
blong

9
$[...]использовался bashдо того, как $((...))был принят в оболочку POSIX. Я не уверен, что он когда-либо был официально объявлен устаревшим, но я не могу найти упоминания о нем на bashстранице руководства , и, похоже, он поддерживается только для обратной совместимости.
chepner

Кроме того, $ (...) предпочтительнее...
Леннарт Роллан

7
@blong Вот вопрос SO о $ [...] vs $ ((...)), который обсуждает и ссылается на устаревание: stackoverflow.com/questions/2415724/…
Ogre Psalm33

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

ПРОВЕРЕННАЯ BASH: Centos, SuSE, RH


1
@kroonwijk перед квадратной скобкой должен быть пробел (формально «разграничивать слова»). В противном случае Bash не сможет увидеть конец предыдущего выражения.
Эдвард Гарсон

1
вопросы были о некотором времени с каналом, поэтому, если создается подоболочка, ваш ответ правильный, но вы не используете канал, поэтому он не отвечает на вопрос
chrisweb

2
Комментарий Чепнера к другому ответу, $[ ]синтаксис устарел. stackoverflow.com/questions/10515964/…
Марк Хаферкамп 08

это не
решает

42
COUNTER=$((COUNTER+1)) 

- довольно неуклюжая конструкция в современном программировании.

(( COUNTER++ ))

выглядит более «современно». Вы также можете использовать

let COUNTER++

если вы думаете, что это улучшает читаемость. Иногда Bash предоставляет слишком много способов делать что-то - я полагаю, философия Perl - когда, возможно, Python «есть только один правильный способ сделать это» может быть более подходящим. Это спорное утверждение, если когда-либо был один! В любом случае, я бы предложил цель (в данном случае) не просто увеличить переменную, но (общее правило) также написать код, который кто-то может понять и поддержать. Соответствие имеет большое значение для достижения этого.

НТН


Это не решает исходный вопрос, а именно, как получить обновленное значение в счетчике ПОСЛЕ завершения цикла (подпроцесса)
Луис Васкес

16

Попробуй использовать

COUNTER=$((COUNTER+1))

вместо того

COUNTER=$((COUNTER))

8
или простоlet "COUNTER++"
нульпотент

2
Извините, это была опечатка. На самом деле ((COUNTER + 1))
Спарш Гупта

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

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

Может быть, ваша строка hash bang запускает 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 в основном удалит все после end = 1 и поместит новый end = 1 в конец (чтобы в следующий раз мы могли удалить все, что добавляется после него).
Спарш Гупта

1
@SparshGupta, предыдущая awk ничего не печатает после "end = 1".
Гленн Джекман 09

Это очень хорошее улучшение для сценария вопросов, но не решает проблему с увеличением счетчика внутри подоболочки
Зник


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[@]}"(вместо звездочки).
Приостановлено до дальнейшего уведомления.

опечаток. в bash (а не в perl, как я писал ранее по ошибке) код выхода представляет собой таблицу, тогда вы можете отдельно проверить все коды выхода в цепочке каналов. перед первым тестированием вы должны скопировать эту таблицу, иначе после первой команды вы потеряете все значения.
Znik

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


3

Это все, что вам нужно сделать:

$((COUNTER++))

Вот отрывок из книги Learning the bash Shell , 3rd Edition, pp. 147, 148:

Арифметические выражения bash эквивалентны своим аналогам в языках Java и C. [9] Приоритет и ассоциативность такие же, как в C. Таблица 6-2 показывает поддерживаемые арифметические операторы. Хотя некоторые из них являются (или содержат) специальные символы, нет необходимости экранировать их с помощью обратной косой черты, поскольку они находятся в синтаксисе $ ((...)).

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

Операторы ++ и - полезны, когда вы хотите увеличить или уменьшить значение на единицу. [11] Они работают так же, как в Java и C, например, value ++ увеличивает значение на 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
Bruno Bronosky

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

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

Это не решает вопрос о получении значения счетчика вне цикла.
Луис Васкес

1

Это простой пример

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

1
простой пример, но не применимый к вопросу.
Znik

0

Похоже, вы не обновляли counterскрипт, используйтеcounter++


Приносим извинения за опечатку, я на самом деле использую ((COUNTER + 1)) в скрипте, который не работает
Спарш Гупта

не имеет значения, увеличивается ли он на значение +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.