Вывод сообщений статуса в bash скрипте


firex - Posted on 09 Июнь 2011

При написании shell скриптов часто возникает необходимость отображать действия скрипта и информацию о то выполнилось действие или нет, чтобы примерно выглядело как выполнение init.d скриптов при загрузке системы.

Примерно вот так:

 * Производится действие 1          [DONE]
 * Производится действие 2          [FAIL]
это ошибка действия 2

в совмесной работе с ramok была созданая следующая функция

show_progress(){
   m=${1}
   l=""
   n=0
   while read -r f ; do
       n=$((${n} + 1))
       l="${n}/${m}"
       # output progress in case the parameter is defined
       test ${m} -ne 0 && echo -en "$(tput sc)$(tput hpa $(tput cols))$(tput cub ${#l})$l$(tput rc)" 
   done
   # delete progress output
   printf "$(tput sc)$(tput hpa $(tput cols))$(tput cub ${#l})%${#l}s$(tput rc)"
}
 
action(){
    local bold=$(tput bold)
    local yellow=$(tput setf 6)
    local red=$(tput setf 4)
    local green=$(tput setf 2)
    local reset=$(tput sgr0)
    local toend=$(tput hpa $(tput cols))$(tput cub 6)
    local max_progress=0
    local LOGFILE=$(mktemp)
    local ACTRESULT=$(mktemp)
    trap "rm -f ${LOGFILE} ${ACTRESULT}" EXIT
 
    if [[ $# = 3 ]]; then max_progress=$3; fi
    echo -ne " ${bold}${yellow}*${reset} ${bold}${1}"
    eval "$2 ; echo "\$?" >${ACTRESULT}; " 2>${LOGFILE} | show_progress ${max_progress}
    result=$(cat ${ACTRESULT})
    rm -f ${ACTRESULT}
    if [[ ${result} != 0 ]]; then
        echo -e "${red}${toend}[FAIL]"
    else
        echo -e "${green}${toend}[DONE]"
    fi
    echo -ne "${reset}"
    cat ${LOGFILE} 2>/dev/null
    return $result
}
 
 
 
do1(){
    return 0
}
do2(){
    echo "1. это вывод действия 2" ; sleep 1
    echo "2. это вывод действия 2" ; sleep 1
    echo "3. это вывод действия 2" ; sleep 1
    echo "4. это вывод действия 2" ; sleep 1
    echo "это ошибка действия 2" >&2
    return 1
}
action "Выполняется действие 1" do1
action "Выполняется действие 2" do2 4

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

у этой функции есть побочный эффект. если в скрипте установлен обработчик прерывания то после вызова функции action этот обработчик перестанет действовать.

скрипт публикуется как public domain.

Edit:

  • добавлена функциональность позволяющая выводить примитивный стаус прогресса выполняемого процесса по средству подсчёта выведённых строк в устройство стандартного вывода.
  • В лог файл записывается только строки направленые в устройство стандартного вывода ошибок.
  • 4.5
    Ваша оценка: Ничего Рейтинг: 4.5 (2 голоса)

    через PuTTY скрипт отображает не правильно... :-( наверное нада выбрать какую нибудь другую эмуляцию терминала.

    Edit:

  • добавлена функциональность позволяющая выводить примитивный стаус прогресса выполняемого процесса по средству подсчёта выведённых строк в устройство стандартного вывода.
  • В лог файл записывается только строки направленые в устройство стандартного вывода ошибок.
  • Скрипт хороший, только пара замечаний:
    1) Не очень логично смешивать tput и явное использование управляющих последовательностей терминала.
    Следовало бы писать local bold=$(tput bold), local yellow=$(tput setf 6), local toend=$(tput hpa $(tput cols))$(tput cub 6) и т.д.

    2) Неплохо бы добавить проверку [ -t 1 ], чтобы нормально работало перенаправление вывода скрипта в файл.

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

    1. действительно не логично. исправил.
    2. я не въехал где эту проверку ставить.

    с выводом мы с ramok пытались организовать по другому пока не вышло. нужна помощь.

    да мы в man 5 terminfo нашли сохранение текущего восстановления положения курсора и его восстановление. Но проблема в том что сохранять положение курсора нужно два раза:
    1. вывели "* Производится действие 1"
    2. сохранили текущее положение курсора (что бы потом вывести в этой строке DONE или FAIL
    3. запустить программу которая что то вывоводит.
    4. сохранить положение курсора еще раз
    5. прыгнуть обратно на первую сохранённую позицию, вывести FAIL или DONE
    6. прыгнуть обратно на сохранённую позицию сделаную на шаге 4

    А команды tput sc и tput rc не позволяют сохнатять и восстанавливать два положения подряд.

    Да, запоминать сразу несколько позиций нельзя, поэтому я имел в виду другое - подсчитывать выводимые на экран строки и позиционировать курсор обратно на известное число строк. Впрочем, я модифицировал скрипт так, что ему не требуется временный файл. Изменения достаточно очевидны, только два места, пожалуй, заслуживают комментария.

    1) Это можно было бы оформить как отдельный типс: как обработать одновременно и stdout, и stderr программы разными скриптами без использования временных файлов. Вот код:

    exec 3>&1
    (программа | обработчик_stdout) 2>&1 >&3 | обработчик_stderr
    exec 3>&-  # закрываем дескриптор 3

    В скрипте я еще запихиваю в stderr результат выполнения команды с помощью

    ($2; echo $? >&2)

    Функция show_status извлекает этот результат с помощью tail -n 1.

    2) Функция show_progress была модифицирована. Одно изменение тривиально - в случае если нам не известно заранее количество строк в выводе программы show_progress теперь выводит "песочные часы" - вращающуюся палочку. Второе изменение связано с тем, что теперь stdout у функции show_progress не направлен непосредственно на терминал, а на дескриптор с номером 3, который выводится на терминал. В результате tput не может определить ширину терминала, т.е. tput cols всегда возвращает 80, как если бы вывод был перенаправлен в файл.
    Чтобы избежать проблем я экспортировал переменную COLUMNS из внешнего скрипта.

    В общем, вот обновленный скрипт:

    show_progress(){
       local m=${1}
       local l=""
       local n=0
       while read -r f ; do
           n=$((n + 1))
           # output progress in case the parameter is defined, otherwise show a sandclock
           if [ ${m} -ne 0 ] ; then
               l="${n}/${m}"
           else
    	   n=$((n % 4))
    	   case $n in
    	       0) l="-" ;;
    	       1) l="\\" ;;
    	       2) l="|" ;;
    	       3) l="/" ;;
    	   esac
           fi
           echo -en "$(tput sc)$(tput hpa ${COLUMNS})$(tput cub ${#l})$l$(tput rc)" 
       done
       # delete progress output
       printf "$(tput sc)$(tput hpa ${COLUMNS})$(tput cub ${#l})%${#l}s$(tput rc)"
    }
     
    show_status(){
        local stderr=$(cat)
        # the last line contains the exit code of the action
        local result=$(echo "$stderr" | tail -n 1)
        # strip the result from stderr
        stderr=$(echo "$stderr" | sed -ne '$q;p')
        if [[ ${result} -ne 0 ]]; then
            echo -e "${bold}${red}${toend}[FAIL]"
        else
            echo -e "${bold}${green}${toend}[DONE]"
        fi
        echo -ne "${reset}"
        test -n "${stderr}" && echo "$stderr"
        return ${result}
    }
     
    action(){
        local bold=$(tput bold)
        local yellow=$(tput setf 6)
        local red=$(tput setf 4)
        local green=$(tput setf 2)
        local reset=$(tput sgr0)
        export COLUMNS=$(tput cols)
        local toend=$(tput hpa ${COLUMNS})$(tput cub 6)
        local max_progress=${3:-0}
     
        echo -ne " ${bold}${yellow}*${reset} ${bold}${1}${reset}"
        exec 3>&1 
        (($2; echo $? >&2) | show_progress ${max_progress}) 2>&1 >&3 | show_status
        exec 3>&-
    }
     
    do1(){
        for i in $(seq $((RANDOM/3000 + 1))) ; do
                echo "это вывод действия 1"; sleep 1
        done            
        return 0
    }
    do2(){
        echo "stdout1: это вывод действия 2" ; sleep 1
        echo "stdout2: это вывод действия 2" ; sleep 1
        echo "stderr1: это ошибка действия 2" >&2
        echo "stdout3: это вывод действия 2" ; sleep 1
        echo "stdout4: это вывод действия 2" ; sleep 1
        echo "stderr2: это ошибка действия 2" >&2
        return 1
    }
     
    action "Выполняется действие 1" do1
    action "Выполняется действие 2" do2 4

    предлагаю заменить песочные часы

    	   case $n in
    	       0) l="-" ;;
    	       1) l="\\" ;;
    	       2) l="|" ;;
    	       3) l="/" ;;
    	   esac

    на более элегантное, как мне кажется, решение

    str='/-\|'
    printf '%s\b' "${str:$n:1}"

    чесно говоря я не совсем понял модификации. Нужно въехать во все детали, но по-моему обработать stdout и stderr можно без открытия дополнительного дескриптора.

    программа >(обработчик stdout) 2>(обработчик stderr)

    по поводу песочных часов я как раз совсем недавно опубликовал типс :-)
    http://linsovet.com/sh-wait-progress-bar но это к делу не отностится. у меня была идея что не каждой программе нужно показывать прогресс. многие программы выполняются достаточно быстро. по-этому если не передавать третий параметр то это означает что прогресс показывать не нужно. Хотя если прорамма выполнится быстро то и песочные часы пользователь не успеет заметить. Думаю это полезное улучшение.

    Благодарю за улучшение скрипта.

    Этот вариант с перенаправлением работает только если доступно process substitution, что есть не на всех шеллах и даже в баше не всегда включено. Кроме того, "программа" должна быть модифицирована соответствующим образом, так как сама по себе конструкция "программа >(обработчик)" не перенаправит stdout программы на вход обработчика. Программа должна понять, что ей надо выводить данные в соответствующую fifo, иначе ничего не будет. Как, например, обработать результат ls? ls >(cat) выдаст просто что-то вроде /dev/fd/63.

    программа будет выводить стандартный вывод на вход программы если я исправлю строчку

    программа > >(обработчик stdout) 2> >(обработчик stderr)

    по поводу того что фича process substitution может быть отключена мне ничего об этом не извесно. спасибо за коментарий. я поищу по этому поводу что сказано в документации. я думал это стандартная "фишка" как перенаправление вывода в файл.
    я попробовал следующую конструкцию и она работает

    cat myfile > >(wc -l)

    и она отработала как и ожидалось.

    Отправить комментарий

    Google Friend Connect (leave a quick comment)
    loading...
    Содержание этого поля является приватным и не предназначено к показу.