Математические операции на массивах от зарегистрированных строк

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

Вот то, с чем я играл:

linesToArraySum() {
while read line
do
    logLine=$line # saves currently logged line in variable logLine
    IFS=';' read -a arrayLog <<< $logLine #redirect variable logLine as input for read command. read -a saves word of input string as array. InternalFieldSeparator set as ';' detects elements in input string which are separated by '; ' as words.
    for n in 1 3 5 7 9 11
    do
        arraySum[n]=$((${arraySum[n]} + ${arrayLog[n]})) # define element in arraySum at position n as sum of previous element and element in arrayLog at this position
        echo ${arraySum[n]}
    done
    return arraySum
done
}

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

while [[ $i < 9 ]] 
do 
    i=$(($i + 1))
    echo "dateTime;110;2930;112;2931;115;2932;112;2933;113;2934;120;2935" | linesToArraySum
done
commandDoSomethingWith_arraySum

Моя проблема состоит в том, что $ эха {arraySum [n]} в функции linesToArraySum () всегда возвращает текущее значение $ {массив [n]} вместо суммы значений в том же столбце.

Я высоко ценил бы любые подсказки к своей ошибке (ошибкам).\o/

2
задан 17 September 2017 в 21:33

2 ответа

В Bash, каждой команде в pipline (|) выполнения в подоболочке. Изменения в переменных в подоболочке, включая присвоения, сделанные к элементам массива, не распространяют назад до родительской оболочки. В Вашем тестовом коде Вы имеете:

echo "dateTime;110;2930;112;2931;115;2932;112;2933;113;2934;120;2935" | linesToArraySum

Функции Shell обычно не работают в подоболочках, но в этом случае Вы работаете linesToArraySum в подоболочке, потому что это появляется в pipline. В некоторых других оболочках, как Ksh, самая правая команда в конвейере не выполняется в подоболочке, и Ваш код будет на самом деле работать в такой оболочке. Но Bash выполняет даже последнюю команду, передаваемую по каналу к в ее собственной подоболочке.

Поскольку linesToArraySum выполнения в подоболочке, arraySum массив существует только в подоболочке, никогда не создается для вызывающей стороны и недавно воссоздается в новой подоболочке каждый раз конвейерные выполнения. Кроме того, даже если бы массив уже существовал, прежде чем подоболочка была запущена, модификации, сделанные к нему в подоболочке, только изменили бы копию подоболочки.

Все, что Вы имеете сделать для решения этой проблемы, должно передать вход linesToArraySum использование метода, который не выполняет функцию в подоболочке. Один способ сделать, который должен использовать здесь строка вместо конвейера:

linesToArraySum <<<"dateTime;110;2930;112;2931;115;2932;112;2933;113;2934;120;2935"

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

for i in {0..9}; do linesToArraySum <<<"dateTime;110;2930;112;2931;115;2932;112;2933;113;2934;120;2935"; done

(Конечно, можно принять решение записать это по нескольким строкам.)


Как упоминания Sergiy Kolodyazhnyy, Вы вызываете свою функцию однажды на строку, вместо того, чтобы передать все это строки. Фиксированный код, который я показал выше, не изменяет это. Так как Вы записали linesToArraySum функционируйте для чтения нескольких строк, Вы могли бы хотеть, чтобы Ваш тестовый код протестировал это. Но это не то, почему значения в arraySum не сохраняются. Первый сценарий Bash в ответе Sergiy Kolodyazhnyy избегает проблемы путем передачи по каналу нескольких строк входа за один раз к функции оболочки, так, чтобы каждая модификация массива произошла в той же подоболочке. Вот почему это работает. Кроме того:

  • После generate_lines | sum_line_tokens управляйте концами, последующие команды все еще не смогут считать суммы из arraySum, так как массив все еще создается в подоболочке, которая уничтожается в конце команды.
  • Пока Вы используете конвейер, создавая arraySum массив прежде, чем вызвать функцию в pipline не будет работать для сохранения значений, также. Подоболочка получит копию arraySum от вызывающей стороны, таким образом, код, который работает в подоболочке, сможет получить доступ к значениям, которые были присвоены ему, но когда это пишет в массив, который будет только влиять на копию подоболочки массива. И если Вы прекращаете вызывать свою функцию в pipline, затем Вы не должны делать ничего больше, чтобы заставить ее работать!

Та вторая точка переносит объяснение далее, так как это касается общей точки беспорядка. В Bash, x=foo; IFS= read -r x <<<bar; echo "$x" печать bar, но x=foo; echo bar | IFS= read -r x; echo "$x" печать foo. Помещение их в функции, объявляя переменную с declare или local, и/или использование массива не изменяет основной принцип, что изменение переменной в подоболочке не изменяет его для вызывающей стороны. Например, предположите выполнение этого определения:

 f() { local -ai a=(10 20 30); g() { IFS= read -r 'a[3]'; echo "${a[@]}"; }; echo 40 | g; echo "${a[@]}"; }

Затем выполненный f. Вывод показывает что массив a изменяется в конвейере где функция g назван, но модификация не сохраняется после echo 40 | g команда:

10 20 30 40
10 20 30

Причина, которая второй сценарий Bash в работах ответа Sergiy Kolodyazhnyy просто, что это избегает использования конвейера и таким образом старается не выполнять sum_line_tokens функция в подоболочке. Путем это делает это - вход взятия, перенаправленный из файла (< "$tempfile") вместо того, чтобы использовать канал:

generate_lines > "$tempfile"
sum_line_tokens "$1" < "$tempfile"

Тот сценарий содержит комментарий, объясняя это sum_line_tokens будет работать в подоболочке, если Вы будете использовать ее в конвейере, как generate_lines | sum_line_tokens. Тот комментарий является на самом деле ответом на Ваш целый вопрос. Другие изменения в том сценарии - пишущий a main() функция, создавая массив явно прежде, чем вызвать функции, которые используют его, и использование local встроенный, чтобы сделать это - абсолютно не важно. (Что сценарий в целом все еще полезен, тем не менее, и в котором он показывает один способ избегать использования конвейера и в котором он показывает путь к связанному с реализацией поведению, о котором Вы спросили в комментариях.)

Когда Вы воздерживаетесь от размещения команды в pipline, чтобы препятствовать тому, чтобы он работал в подоболочке, какая альтернатива, которую Вы выбираете, будет зависеть от обстоятельств. Для текста, который появляется в Вашем сценарии, используйте здесь строка (как показано выше) или здесь документ. Для вывода от другой команды, пишущий во временный файл и затем читая из него - как во втором сценарии Bash Sergiy Kolodyazhnyy - часто разумный выбор. Вы могли даже создать временный файл как именованный канал с mkfifo вместо регулярного файла, если Вы хотите, чтобы это имело идентичную семантику и подобные рабочие характеристики к конвейеру оболочки. Но в большинстве случаев, я рекомендую использовать замену процесса, которая на самом деле создает, использует и уничтожает именованный канал для Вас, все негласно:

sum_line_tokens "$1" < <(generate_lines)

Выполнять ту команду, оболочку:

  • Создает временный именованный канал.
  • Выполнения generate_lines и перенаправляет его вывод к именованному каналу.
  • Замены <(generate_lines) с названием именованного канала.
  • Выполнения sum_line_tokens "$1" и вход перенаправлений от именованного канала до него (из-за <).

Команда, пишущая в именованный канал на самом деле, работает в то же время, что и команда, читающая из именованного канала. Распоряжение, данное выше, для концептуальной простоты (я должен был записать им в некотором порядке). Обратите внимание также что:

  • Первое < для перенаправления ввода и второго < это - часть синтаксиса замены процесса, должен быть разделен. Это должно сказать это, где ... управление, от которого Вы хотите принять вход, записать < <(...), нет <<(...).
  • Замена процесса действительно использует подоболочку - но только для процесса, которым заменяют. Таким образом generate_lines команда выполняется в подоболочке, но sum_line_tokens не. Если Вы пытались изменить переменные вызывающей стороны в generate_lines, те модификации не сохранились бы впоследствии. Однако generate_lines не должен делать этого. Только sum_line_tokens потребности изменить переменные, которые будут использоваться впоследствии, таким образом, будет достаточно что это не быть выполненным в подоболочке.
  • Замена процесса - а также здесь представляет в виде строки и [[- не являются портативными ко всем оболочкам стиля Границы. (Здесь документы и test/[ являются портативными.), Но массивы не являются портативными также, поэтому, пока Вы используете массив для этого, Вы уже не пишете портативный сценарий - в смысле того, чтобы быть портативным через различные оболочки - таким образом, нет, вероятно, никакой причины для Вас избегать использования замены процесса.

В Вашем сценарии существуют некоторые другие ошибки. Так как их легко сделать в любом сценарии - не только этом - и так как я предполагаю, что Вы пишете этот сценарий в целях практики, я перечислю их здесь. Однако как Sergiy Kolodyazhnyy заявляет, необходимо рассмотреть использование инструмента как awk для этого. Много стандартных утилит Unix существуют главным образом в целях обработки текста линию за линией, и awk один из них.

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

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

  • Как Sergiy Kolodyazhnyy говорит, Вы не можете использовать return к возвращаемым массивам. На самом деле Вы не можете даже возвратить простую переменную. Можно только возвратить код выхода, который должен колебаться от 0 до 255 и не очень универсален. Основная цель передать аргумент return или exit встроенный должен указать, была ли ошибка, или какая из нескольких возможных ошибок произошла, или возвратить одно из маленького небольшого количества возможных сведений. (Например, test/[ код возврата builtin указывает, является ли протестированное условие TRUE или FALSE.) С кодом Вы имеете, необходимо видеть эту ошибку:

    -bash: return: arraySum: numeric argument required
    
  • Необходимо передать -r когда Вы используете read встроенный. Иначе \ Escape расширены. Чрезвычайно редко, чтобы это было тем, что Вы хотели бы. Так использование read -r line вместо read line и используйте read -ra arrayLog (или read -r -a arrayLog, если Вы предпочитаете тот стиль) вместо read -a arrayLog.

  • Даже для чтения строки в единственную переменную установить IFS= если у Вас нет определенной причины, Вы знаете, что Вам не нужно к (или нуждаются не к). Вместо использования while read line, использовать while IFS= read -r line. Причина - это read полосы пробел IFS - что-либо в $IFS- с начала и конца строки это читает. Исключения - то, если Вы на самом деле хотите, чтобы это произошло и - для Bash - при исключении имени переменной. В Bash, read -r без имени переменной эквивалентно IFS= read -r REPLY.

  • Хотя не на самом деле неправильно, Вы не должны использовать полный синтаксис расширения параметра внутри (( )) использовать значения элементов переменных или элементов массива. Предотвращение этого делает такие выражения намного легче читать. Предпочесть $((arraySum[n] + arrayLog[n])) $((${arraySum[n]} + ${arrayLog[n]})).

  • С test, [, и [[, < оператор выполняет лексикографическое сравнение строк и не числовое сравнение. Проверять если $i меньше, чем 9, можно использовать [[ $i -lt 9 ]]. Например, с i=89, [[ $i < 9 ]] возвращает true! Точно так же Вы использовали бы -gt для больше числового - чем, -le для меньше чем или равного числового, и -ge для числового greater-than-or-equal. Или возможно Вы означали писать (($i < 9)), который работал бы, как будет ((i < 9)).

    Однако, так как в этом случае Вы просто хотите циклично выполниться от 1 кому: 9, это намного более просто, более ясно, и легче использовать a for цикл с расширением фигурной скобки ({1..9}) как показано около начала этого сообщения.

Наконец, я рекомендую использовать в своих интересах питание статического анализа кода путем проверки сценариев оболочки с ShellCheck. ShellCheck зафиксирует большинство упомянутых выше ошибок. Многие опытные сценаристы оболочки используют его много, но это очень хорошо для новичков также, потому что это связывается с полными объяснениями каждого из ее правил.

Иногда ShellCheck будет определять что-то как возможно неправильно, которое на самом деле корректно. Например, когда я выполнил его на Вашем сценарии, это повысило SC2086 для <<< $logLine. Строго говоря это не необходимо в версиях Bash, обеспеченного в в настоящее время поддерживаемых системах Ubuntu, потому что текст направо от <<< в здесь строка не подвергается расширению пути или разделению слова. Однако более ранние версии не пропускали те расширения, плюс он - хорошая идея заключить Ваши переменные в кавычки каждый раз, когда у Вас нет определенной причины не к. Это - общий шаблон: даже с некоторыми предупреждениями ShellCheck, что Вы могли безопасно проигнорировать, Вы напишете лучший код, если Вы примете решение учесть их.

2
ответ дан 2 December 2019 в 02:14

Я позволил себе немного отредактировать вашу функцию и записал все в простой скрипт. Суть проблемы заключается в том, что вы должны повторить после завершения цикла while . Кроме того, функции bash не «возвращают» массивы, вы должны каким-то образом выводить их в stdout или использовать функцию main и иметь массив local в main, который затем может быть доступен для дочерних функций (это я часто делаю в моих собственных сценариях).

Вот результат теста. Для 9 итераций, где столбец 1 всегда равен 110, мы соответственно получаем 990.

$ ./generate_lines.sh                                                                                                       
990 1008 1035 1008 1017 1080

И вот сценарий:

#!/usr/bin/env bash

sum_line_tokens() {
while read line
do
    #echo "$line"
    logLine=$line # saves currently logged line in variable logLine
    # redirect variable logLine as input for read command. 
    # read -a saves word of input string as array. InternalFieldSeparator set as ';' 
    # detects elements in input string which are separated by '; ' as words.
    IFS=';' read -a arrayLog <<< $logLine     

    for n in 1 3 5 7 9 11
    do
        # define element in arraySum at position n as sum of previous element 
        # and element in arrayLog at this position
        arraySum[n]=$(( ${arraySum[n]} + ${arrayLog[n]} ))         
        #echo "${arraySum[n]}"
    done
done

# Functions in bash can only use return to indicate exit status
# This is more like int datatype for C or Java functions. If you want
# to return a string or array, you need to echo it to stdout
echo "${arraySum[@]}"
}

generate_lines(){
    while [[ $i < 9 ]] 
    do 
        i=$(($i + 1))
        echo "dateTime;110;2930;112;2931;115;2932;112;2933;113;2934;120;2935"
    done
}

generate_lines |  sum_line_tokens

Упрощение задачи с помощью awk

Хотя скрипт работает, он занимает много времени. Мы можем сократить решение с помощью awk:

# again, same thing - the script now generates lines only, no summing. 
# We'll pipe it to awk
$ ./generate_lines.sh                                                                                                                                                                           
dateTime;110;2930;112;2931;115;2932;112;2933;113;2934;120;2935
dateTime;110;2930;112;2931;115;2932;112;2933;113;2934;120;2935
dateTime;110;2930;112;2931;115;2932;112;2933;113;2934;120;2935
dateTime;110;2930;112;2931;115;2932;112;2933;113;2934;120;2935
dateTime;110;2930;112;2931;115;2932;112;2933;113;2934;120;2935
dateTime;110;2930;112;2931;115;2932;112;2933;113;2934;120;2935
dateTime;110;2930;112;2931;115;2932;112;2933;113;2934;120;2935
dateTime;110;2930;112;2931;115;2932;112;2933;113;2934;120;2935
dateTime;110;2930;112;2931;115;2932;112;2933;113;2934;120;2935

$ ./generate_lines.sh  | awk -F ';' '{for(i=1;i<=11;i++) if(i%2 != 0) sum[i+1]+=$(i+1) }END{for(j in sum) printf "%d\t",sum[j];print ""}'                                                       
990 1008    1035    1008    1017    1080

Используя основную функцию и локальный массив

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

Ну, в вашем случае есть небольшая проблема. У нас есть две функции: одна создает линии, а другая - что-то делает с этими линиями, и использование каналов имеет проблему - все, что выполняется с правой стороны канала, работает в подоболочке, а это означает, что вся информация из подоболочки исчезает при выходе из подоболочки , См. мой старый вопрос для справки.

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

Итак, в этой версии скрипта я рассмотрел несколько вещей, включая функцию main и то, как функция main получает аргументы командной строки, а также то, что вы запрашивали в комментариях. По сути, сценарий теперь получает 1 аргумент командной строки - это количество строк, которое вы хотите, и передает это функции sum_line_tokens. Без аргументов командной строки он суммирует все строки.

Тестовый прогон:

$ ./generate_lines.sh 3                                                                                                                                                                         
330 336 345 336 339 360

$ ./generate_lines.sh 4                                                                                                                                                                         
440 448 460 448 452 480

И сам скрипт:

#!/usr/bin/env bash

sum_line_tokens() {
    # To perform counting for n number of lines, use a counter variable
    # In this case I am using argument passed from command-line
    linecount=0


    # IFS= and -r for better line reading to ensure that spaces won't mess you up
    while IFS='' read -r line
    do
        # Check if we have arg 1 to function and quit after n lines
        if [ -n $1  ] && [ $linecount -eq $1 ] 
        then
            break
        fi

        logLine=$line 
        IFS=';' read -a arrayLog <<< $logLine     


        for n in 1 3 5 7 9 11
        do
            arraySum[n]=$(( ${arraySum[n]} + ${arrayLog[n]} ))         
        done
        # increment line counter
        ((linecount++))
    done
}

generate_lines(){
    while [[ $i < 9 ]] 
    do 
        i=$(($i + 1))
        echo "dateTime;110;2930;112;2931;115;2932;112;2933;113;2934;120;2935"
    done
}

main(){
    # create local array. Any function called from main will know about it
    local -a arraySum

    # We can't just pipe lines to summing function. Whatever runs on the right-hand side
    # of the pipe runs in subshell, which means when that subshell exits, your variables are gone
    # See https://askubuntu.com/q/704154/295286
    tempfile=$(mktemp)
    generate_lines > "$tempfile"
    sum_line_tokens "$1" < "$tempfile"
    echo "${arraySum[@]}"
    rm "$tempfile"
}

# Call main function with the command-line arguments. This works sort of like int main(String[] args) in Java
main "$@"

Замечание о переносимости

Конечно, потому что мы используем много пакетов bash -специальные вещи, если вы запустите это в системе, где bash недоступен, он не будет работать. Это хороший сценарий? Да, это делает работу. Это переносной скрипт? Нет. Решение awk, приведенное выше, возможно, более переносимо.

2
ответ дан 2 December 2019 в 02:14

Другие вопросы по тегам:

Похожие вопросы: