RomeoGolf

Вт 29 Март 2016

Засекаем время в bat/cmd

При всех моих стараниях уйти от Windows везде и навсегда не получается. Есть компьютер на работе, компьютеры друзей, собственный нетбук, с которым неохота возиться настолько кардинально. При этом, ряд задач в принципе выполняется из командной строки проще и удобнее, чем «оконно-мышевым» способом. Особенно если это задачи из тех, что имеют прямое отношение к Linux, например, работа с утилитами из комплекта gnuwin32.

Запуская однажды уже упомянутую упаковку/распаковку при помощи windows-реализации tar, я захотел однажды засечь время. Ну, не секундомером же это делать… Сделал командный файл, в котором вывожу время, запускаю архиватор, снова вывожу время.

@time \t
@tar -cf foo.tar foo
@time \t

Получается ерунда. Команда time \t выдает время с точностью до минуты. Ладно, можно использовать переменную среды %time%:

@echo %time%
@tar -cf foo.tar foo
@echo %time%

Чуть получше, но все равно очень плохо. Во-первых, если архиватору добавить ключ -v, и он начнет выдавать в консоль имена архивируемых объектов, то время начала стремительно унесется за пределы экрана. Во-вторых, чтобы понять, сколько процесс занял времени, все равно придется считать, а компьютер на что?

Однако переменная %time% хитроформатированная, скорее текст, чем число, и ее в лоб не получится использовать для вычитания времени начала из времени окончания. Тем более, командный интерпретатор cmd.exe работает с целочисленными значениями, а время идет с точностью до сотых секунды (ну, или до десятков миллисекунд, как хотите).

Ладно, разберем время на составляющие. При помощи конструкции set t1_m=%t1:~3,2% которая присваивает переменной t1_m два знака из t1, пропустив первые три, получим часы, минуты, секунды и сотые доли секунды:

set t1_h=%t1:~0,2%
set t1_m=%t1:~3,2%
set t1_s=%t1:~6,2%
set t1_ms=%t1:~9%
set /a s1= t1_h * 60 * 60 * 100 + t1_m * 60 * 100 + t1_s * 100 + t1_ms

Напоминаю, что ключик /a позволяет оператору set не только присваивать значение переменной, но и выполнять некоторые операции (подробнее — set /?).

В результате получили в s1 время в сотнях миллисекунд, допустим, из времени начала операции. Можно сделать аналогичным образом s2 из времени окончания. Эти две переменные целочисленные и прекрасно складываются, вычитаются и все такое. Нам надо разность, и, чтобы избежать неожиданностей со знаками, можно сразу определить, что из них больше, и потом вычитать:

if /I %s1% geq %s2% (set /a s3= s1 - s2) else (set /a s3= s2 - s1)

Ключик /i расширяет возможности оператора сравнения, позволяя ему не только выполнять сравнение строк на тождественность (чем он занимался изначально), но и сравнивать числа на больше/меньше/равно (if /?)

Получили s3 — разность времени в десятках миллисекунд. Надо разобрать на составляющие, а потом склеить обратно в удобочитаемом виде. Все несложно, используем остаток от деления, чтобы избавиться, например, от часов при извлечении минут, и деление (целочисленное, другого тут нет), чтобы избавиться от секунд, примерно так:

@set /a t3_h= s3 / 100 / 60 / 60
@set /a t3_m= s3 %% (100 * 60 * 60) / 100 / 60
@set /a t3_s= s3 %% (100 * 60) / 100
@set /a t3_ms= s3 %% 100

Тут надо не забыть забавную тонкость: в командном интерпретаторе символ процента — подводный камень. Иногда он ставится по обе стороны от переменной, и тогда при выполнении интерпретатор отбрасывает эту пару, а имя переменной заменяет значением. Иногда он стоит перед переменной цикла. Иногда он используется в виде оператора получения остатка от целочисленного деления. Так вот, там, где процент непарный, его надо не забывать удваивать, а то его отбросят и будут безуспешно искать пару.

Получили часы, минуты, секунды и десятки миллисекунд в отдельных переменных. Можно их использовать, как числа, а можно — как строки, поэтому их легко склеить в одно значение времени и выдать его в стандартный поток ввода-вывода, «на консоль»:

@set t3=%t3_h%:%t3_m%:%t3_s%,%t3_ms%
@echo %t3%

Все это можно засунуть в отдельный файл, чтобы использовать в разных местах. Тогда время надо будет передавать файлу в виде параметров. Тут есть два варианта: либо

@set t1=%~1
@set t2=%~2

и передавать параметры в двойных кавычках (а тильда раскроет кавычки), либо

@set t1=%1%2
@set t2=%3%4

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

Итого, получился такой командный файл, скажем, timecalc.bat:

@set t1=%~1
@set t2=%~2
@rem параметры должны быть в кавычках

@rem echo t1 = %t1%
@rem set t1=03:02:20,45
@set t1_h=%t1:~0,2%
@set t1_m=%t1:~3,2%
@set t1_s=%t1:~6,2%
@set t1_ms=%t1:~9%
@rem echo %t1% = %t1_h% - %t1_m% - %t1_s% - %t1_ms%
@set /a s1= t1_h * 60 * 60 * 100 + t1_m * 60 * 100 + t1_s * 100 + t1_ms
@rem echo s1 = %s1%

@rem set t2=01:01:10,05
@rem echo t2 = %t2%
@set t2_h=%t2:~0,2%
@set t2_m=%t2:~3,2%
@set t2_s=%t2:~6,2%
@set t2_ms=%t2:~9%
@rem echo %t2% = %t2_h% - %t2_m% - %t2_s% - %t2_ms%
@set /a s2= t2_h * 60 * 60 * 100 + t2_m * 60 * 100 + t2_s * 100 + t2_ms
@rem echo s2 = %s2%

@if /I %s1% geq %s2% (set /a s3= s1 - s2) else (set /a s3= s2 - s1)
@rem set /a s3= s2 - s1
@rem echo s3 = %s3%

@set /a t3_h= s3 / 100 / 60 / 60
@set /a t3_m= s3 %% (100 * 60 * 60) / 100 / 60
@set /a t3_s= s3 %% (100 * 60) / 100
@set /a t3_ms= s3 %% 100
@rem echo = %t3_h% - %t3_m% - %t3_s% - %t3_ms%

@set t3=%t3_h%:%t3_m%:%t3_s%,%t3_ms%
@echo %t3%

Теперь об использовании. Допустим, есть такой батничек, запускающий архивацию с засечкой времени:

@set a=%time%
@echo start %a%
@tar -cf %1.tar %1
@set b=%time%
@echo stop %b%

В переменных a и b имеем время начала и окончания операции соответственно. Можно передать их нашему скрипту, непременно валяющемуся тут же рядом, в этой же папке:

call .\timecalc.cmd "%b%" "%a%"

И он выведет в консоль затраченное время третьей строчкой, после «start <время начала>» и «stop <время окончания>». Но хотелось бы добавить, допустим, «упаковано за » или «распаковано за », так что ввести соответствующие строки в файл «подпрограммы» не получится. Передавать этот строковый «довесок» в виде еще одного параметра и не особо красиво, и при желании дописать текст после времени (а не перед) опять потребует лезть в вызываемый файл. А как перехватить вывод вызываемого файла для использования в вызывающем?

Я просто обалдел, когда узнал. Внезапно, при помощи оператора цикла!!! О, сколько нам открытий чудных готовит for /?

Расширение синтаксиса /F позволяет выковыривать переменную цикла из разного рода наборов: файлов, символьных строк, команд. Чтобы понять, что используется для цикла, применяются (или не применяются) кавычки. Без кавычек в скобках пишем файл или набор файлов. А применение кавычек зависит от того, установлена или нет опция usebackq: при установленной в двойных кавычках пишем строку, в простых — команду. При снятой опции строку пишем в простых кавычках, а команду — в обратных (которые обычно на клавише с буквой Ё). При этом, команда вполне может быть одна, и проход цикла тоже один, и в теле цикла получаем нужный нам вывод команды, который засунем в переменную для дальнейшего использования (хотя можно и использовать сразу):

@for /F "usebackq" %%s in (`call .\timecalc.cmd "%b%" "%a%"`) do @set delta=%%s

И, кстати, не забываем, что переменная цикла должна быть односимвольной. Нигде этого не прочитал, вывел экспериментально путем ошибок и проб. Может, читал невнимательно, а может, не знал где…

Итого получаем такой командный файл, скажем, tar_time.cmd, который принимает в качестве параметра пакуемую папку, засекает время и пишет его в консоль и в лог-файл:

@echo %1 >> tar_log.txt

@set a=%time%
@echo start %a%
@echo    start %a% >> tar_log.txt
@tar -cf %1.tar %1
@set b=%time%
@echo    stop %b% >> tar_log.txt
@echo stop %b%

@for /F "usebackq" %%s in (`call .\timecalc.cmd "%b%" "%a%"`) do @set delta=%%s

@echo    %delta% >> tar_log.txt
@echo packed for %delta%

Аналогичным образом написал почти то же для распаковки, поправив пару строчек.

Конечно, можно кое-что оптимизировать, например, все вычисления с константами сделать на калькуляторе (или в уме), преобразование времени в сотые доли секунды вынести в «функцию» и вызывать ее через call, но так удобочитаемее, а на скорость выполнения влияет мало. Еще можно вместо своры собак (@) поставить один раз в начале файла @echo off, дело вкуса. И, конечно, можно повнимательнее относиться к регистру символов и писать либо везде прописными, либо везде строчными. Но, как уж получилось…

Возможности команд Windows (тяжелое наследие DOS), конечно, довольно ограничены, но даже с их помощью можно делать интересные и полезные штучки. Вот только иногда фиг догадаешься, описание какой команды для твоей цели нужно смотреть и где именно искать…


Теги: