Используйте PS0 и PS1 для отображения времени выполнения каждой команды bash

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

user@machine ~/tmp
$ sleep 1

user@machine ~/tmp 1.01s
$

Тем не менее, я быстро застрял со временем записи в PS0, так как что-то вроде этого не работает:

PS0='$(START=$(date +%s.%N))'

Насколько я понимаю, START присваивание происходит в под-оболочке, поэтому оно не видно во внешней оболочке. Как бы вы подошли к этому?

6 ответов

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

Во-первых, я создал несколько функций, которые считывают / записывают время в / из памяти. Запись в папку с общей памятью предотвращает доступ к диску и не сохраняется при перезагрузке, если файлы по какой-то причине не очищены

function roundseconds (){
  # rounds a number to 3 decimal places
  echo m=$1";h=0.5;scale=4;t=1000;if(m<0) h=-0.5;a=m*t+h;scale=3;a/t;" | bc
}

function bash_getstarttime (){
  # places the epoch time in ns into shared memory
  date +%s.%N >"/dev/shm/${USER}.bashtime.${1}"
}

function bash_getstoptime (){
  # reads stored epoch time and subtracts from current
  local endtime=$(date +%s.%N)
  local starttime=$(cat /dev/shm/${USER}.bashtime.${1})
  roundseconds $(echo $(eval echo "$endtime - $starttime") | bc)
}

Входными данными для функций bash_ является PID bash.

Эти и следующие функции добавляются в файл ~/.bashrc

ROOTPID=$BASHPID
bash_getstarttime $ROOTPID

Они создают начальное значение времени и сохраняют PID bash как другую переменную, которую можно передать функции. Затем вы добавляете функции в PS0 и PS1

PS0='$(bash_getstarttime $ROOTPID) etc..'
PS1='\[\033[36m\] Execution time $(bash_getstoptime $ROOTPID)s\n'
PS1="$PS1"'and your normal PS1 here'

Теперь он сгенерирует время в PS0 до обработки ввода терминала и снова сгенерирует время в PS1 после обработки ввода терминала, затем вычислит разницу и прибавит к PS1. И, наконец, этот код очищает сохраненное время при выходе из терминала:

function runonexit (){
  rm /dev/shm/${USER}.bashtime.${ROOTPID}
}

trap runonexit EXIT

Собираем все вместе, плюс тестируем дополнительный код, и это выглядит так:

Важными частями являются время выполнения в мс и файлы user.bashtime для всех активных PID терминалов, хранящихся в общей памяти. PID также отображается сразу после ввода терминала, поскольку я добавил его отображение в PS0, и вы можете видеть добавленные и удаленные файлы bashtime.

PS0='$(bash_getstarttime $ROOTPID) $ROOTPID experiments \[\033[00m\]\n'

Как сказал @tc , использование арифметического расширения позволяет вам назначать переменные во время расширения и. Более новые версии bash также позволяют PS*расширение стиля, поэтому вам даже не понадобится подоболочка для получения текущего времени. С bash 4.4:

      # PS0 extracts a substring of length 0 from PS1; as a side-effect it causes
# the current time as epoch seconds to PS0time (no visible output in this case)
PS0='\[${PS1:$((PS0time=\D{%s}, PS1calc=1, 0)):0}\]'
# PS1 uses the same trick to calculate the time elapsed since PS0 was output.
# It also expands the previous command's exit status ($?), the current time
# and directory ($PWD rather than \w, which shortens your home directory path
# prefix to "~") on the next line, and finally the actual prompt: 'user@host> '
PS1='\nSeconds: $((PS1calc ? \D{%s}-$PS0time : 0)) Status: $?\n\D{%T} ${PWD:PS1calc=0}\n\u@\h> '

%N Директива date, похоже, не реализована как часть \D{...}расширение с помощью bash 4.4. Это очень жаль, поскольку у нас есть только разрешение в единицах секунды.)

Поскольку оценивается и печатается только при наличии команды для выполнения, для флага устанавливается значение 1 делать разницу во времени (следуя команде) в расширении или нет (будучи средством PS0 не был ранее расширен и поэтому не переоценивал PS1time). PS1 затем сбрасывает PS1calc к 0. Таким образом, пустая строка (просто нажатие клавиши возврата) не накапливает секунды между нажатиями клавиши возврата.

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

Арифметическое раскрытие выполняется в текущем процессе и может присваиваться переменным. Он также производит вывод, который вы можете использовать с чем-то вроде\e[$((...,0))m (выводить \e[0m) или ${t:0:$((...,0))}(ничего не выводить, что, по-видимому, лучше). Поддержка 64-битных целых чисел в Bash будет считать наносекунды POSIX до 2262 года.

$ PS0='${t:0:$((t=$(date +%s%N),0))}'
$ PS1='$((( t )) && printf %d.%09ds $((t=$(date +%s%N)-t,t/1000000000)) $((t%1000000000)))${t:0:$((t=0))}\n$ '
0.053282161s
$ sleep 1
1.064178281s
$ 

$ 

PS0 не оценивается для пустых команд, что оставляет пустую строку (я не уверен, что вы можете условно распечатать \n, не нарушая работу). Вы можете обойти это, переключившись на PROMPT_COMMAND вместо этого (который также сохраняет вилку):

$ PS0='${t:0:$((t=$(date +%s%N),0))}'
$ PROMPT_COMMAND='(( t )) && printf %d.%09ds\\n $((t=$(date +%s%N)-t,t/1000000000)) $((t%1000000000)); t=0'
0.041584565s
$ sleep 1
1.077152833s
$ 
$ 

Тем не менее, если вам не требуется субсекундная точность, я бы предложил использовать $SECONDS вместо этого (что также с большей вероятностью даст разумный ответ, если что-то установит время).

Я воспринял это как загадку и хочу показать результат своей загадки:

Сначала я возился с измерением времени. date +%s.%N (что я не осознавал раньше), откуда я начал. К сожалению, похоже, что bash Кажется, что арифметическая оценка не поддерживает плавающие точки. Таким образом, я выбрал что-то еще:

$ START=$(date +%s.%N)

$ awk 'BEGIN { printf("%fs", '$(date +%s.%N)' - '$START') }' /dev/null
8.059526s

$

Этого достаточно, чтобы вычислить разницу во времени.

Далее я подтвердил то, что вы уже описали: вызов под-оболочки предотвращает использование переменных оболочки. Таким образом, я подумал о том, где еще можно хранить время запуска, которое является глобальным для подоболочек, но достаточно локальным для одновременного использования в нескольких интерактивных оболочках. Мое решение временное. файлы (в /tmp). Чтобы предоставить уникальное имя, я придумал этот шаблон: /tmp/$USER.START.$BASHPID,

$ date +%s.%N >/tmp/$USER.START.$BASHPID ; \
> awk 'BEGIN { printf("%fs", '$(date +%s.%N)' - '$(cat /tmp/$USER.START.$BASHPID)') }' /dev/null
cat: /tmp/ds32737.START.11756: No such file or directory
awk: cmd. line:1: BEGIN { printf("%fs", 1491297723.111219300 - ) }
awk: cmd. line:1:                                              ^ syntax error

$

Черт! Снова я пойман в ловушку в проблеме sub-shell. Чтобы обойти это, я определил еще одну переменную:

$ INTERACTIVE_BASHPID=$BASHPID

$ date +%s.%N >/tmp/$USER.START.$INTERACTIVE_BASHPID ; \
> awk 'BEGIN { printf("%fs", '$(date +%s.%N)' - '$(cat /tmp/$USER.START.$INTERACTIVE_BASHPID)') }' /dev/null
0.075319s

$

Следующий шаг: возьмите это вместе с PS0 а также PS1, В похожей головоломке ( ТАК: Как изменить цвет приглашения bash на основе кода завершения последней команды?) Я уже освоил "ад цитирования". Таким образом, я должен быть в состоянии сделать это снова:

$ PS0='$(date +%s.%N >"/tmp/${USER}.START.${INTERACTIVE_BASHPID}")'

$ PS1='$(awk "BEGIN { printf(\"%fs\", "$(date +%s.%N)" - "$(cat /tmp/$USER.START.$INTERACTIVE_BASHPID)") }" /dev/null)'"$PS1"
0.118550s
$

Ааа. Это начинает работать. Таким образом, существует только одна проблема - найти правильный стартовый скрипт для инициализации INTERACTIVE_BASHPID, я нашел ~/.bashrc который кажется правильным для этого, и который я уже использовал в прошлом для некоторых других личных настроек.

Итак, все это вместе - вот те строки, которые я добавил к своему ~/.bashrc:

# command duration puzzle
INTERACTIVE_BASHPID=$BASHPID
date +%s.%N >"/tmp/${USER}.START.${INTERACTIVE_BASHPID}"
PS0='$(date +%s.%N >"/tmp/${USER}.START.${INTERACTIVE_BASHPID}")'
PS1='$(awk "BEGIN { printf(\"%fs\", "$(date +%s.%N)" - "$(cat /tmp/$USER.START.$INTERACTIVE_BASHPID)") }" /dev/null)'"$PS1"

3-я строка (date команда) была добавлена ​​для решения другой проблемы. Прокомментируйте это и начните новый интерактивный bash, чтобы узнать почему.

Снимок моего Cygwin Xterm с Bash, где я добавил вышеупомянутые строки в ./~bashrc: Снимок моего cygwin xterm с

Заметки:

  1. Я считаю это скорее решением головоломки, чем "серьезным продуктивным" решением. Я уверен, что этот вид измерения времени потребляет много времени. time Команда может предоставить лучшее решение: SE: Как эффективно получить время выполнения скрипта?, Тем не менее, это была хорошая лекция для практики Баш...

  2. Не забывайте, что этот код загрязняет ваш /tmp каталог с растущим количеством небольших файлов. Либо убирать /tmp время от времени или добавьте соответствующие команды для очистки (например, ~/.bash_logout).

Как правильно указано в вопросе, PS0 выполняется внутри суб-оболочки, что делает его непригодным для установки времени начала.

Вместо этого можно использовать history команда с эпохой в секундах %s и встроенная переменная $EPOCHSECONDS чтобы вычислить, когда команда закончилась, используя только $PROMPT_COMMAND.

      # Save start time before executing command (does not work due to PS0 sub-shell)
# preexec() {
#   STARTTIME=$EPOCHSECONDS
# }
# PS0=preexec

# Save end time, without duplicating commands when pressing Enter on an empty line
precmd() {
    local st=$(HISTTIMEFORMAT='%s ' history 1 | awk '{print $2}');
    if [[ -z "$STARTTIME" || (-n "$STARTTIME" && "$STARTTIME" -ne "$st") ]]; then
        ENDTIME=$EPOCHSECONDS
        STARTTIME=$st
    else
        ENDTIME=0
    fi
}

__timeit() {
    precmd;
    if ((ENDTIME - STARTTIME >= 0)); then
        printf 'Command took %d seconds.\n' "$((ENDTIME - STARTTIME))";
    fi

    # Do not forget your:
    #     - OSC 0 (set title)
    #     - OSC 777 (notification in gnome-terminal, urxvt; note, this one has preexec and precmd as OSC 777 features)
    #     - OSC 99 (notification in kitty)
    #     - OSC 7 (set url) - out of scope for this question
}

export PROMPT_COMMAND=__timeit

Примечание: если у вас есть ignoredups в твоей $HISTCONTROL, то это не будет возвращать отчет для команды, которая запускается повторно.

После использования переменных @SherylHohman в PS0 я пришел с этим полным сценарием. Я видел, что тебе не нуженPS0Timeпометить какPS0Calcне существует в пустых подсказках, поэтому_elapsedфункция просто выход.

      #!/bin/bash

# string preceding ms, use color code or ascii
_ELAPTXT=$'\E[1;33m \uf135 '

# extract time
_printtime () {
    local _var=${EPOCHREALTIME/,/};
    echo ${_var%???}
}

# get diff time, print it and end color codings if any
_elapsed () {
    [[ -v "${1}" ]] || ( local _VAR=$(_printtime);
    local _ELAPSED=$(( ${_VAR} - ${1} ));
    echo "${_ELAPTXT}$(_formatms ${_ELAPSED})"$'\n\e[0m' )
}

# format _elapsed with simple string substitution
_formatms () {
    local _n=$((${1})) && case ${_n} in
        ? | ?? | ???)
            echo $_n"ms"
        ;;
        ????)
            echo ${_n:0:1}${_n:0,-3}"ms"
        ;;
        ?????)
            echo ${_n:0:2}","${_n:0,-3}"s"
        ;;
        ??????)
            printf $((${_n:0:3}/60))m+$((${_n:0:3}%60)),${_n:0,-3}"s"
        ;;
        ???????)
            printf $((${_n:0:4}/60))m$((${_n:0:4}%60))s${_n:0,-3}"ms"
        ;;
        *)
            printf "too much!"
        ;;
    esac
}

# prompts
PS0='${PS1:(PS0time=$(_printtime)):0}'
PS1='$(_elapsed $PS0time)${PS0:(PS0time=0):0}\u@\h:\w\$ '

изображение результата

Сохраните его как _prompt и попробуйте:source _prompt

Изменить текст, коды ascii и цвета в _ELAPTXT_ELAPTXT='\e[33m Elapsed time: '

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