Используйте 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
:
Заметки:
Я считаю это скорее решением головоломки, чем "серьезным продуктивным" решением. Я уверен, что этот вид измерения времени потребляет много времени.
time
Команда может предоставить лучшее решение: SE: Как эффективно получить время выполнения скрипта?, Тем не менее, это была хорошая лекция для практики Баш...Не забывайте, что этот код загрязняет ваш
/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: '