cmd
cmd.sh ★
Я тут случайно в спор ввязался — можно ли переписать
Речь шла о вполне определённом файле, там запрограммировано небольшое меню и в зависимости от выбранного пункта запускается та или иная, одинаковая для всех ОС, последовательность команд.
Мой собеседник мне не верил, когда я утверждал, что это вполне возможно, и требовал доказательств.
Что ж. Основная моя идея была в том, чтобы заменить команды батника на вызовы
То, что мне предстояло адаптировать начиналось вполне стандартно:
@ECHO off
CHCP 1251
CLSЧто тут происходит? Выключается вывод команд на экран (при адаптации это можно игнорировать), ставится однобайтовая кодировка CP1251 (это хорошо бы учесть) и очищается экран.
Поскольку «баш» этих команд не знает, надо определить три функции, спрятав их от cmd.exe. Сделать это несложно. Команду «двоеточие» cmd.exe трактует как начало метки и на дальнейшее не реагирует, а «баш» считает её пустой командой, которую можно точкой с запятой отделить от строки, которую мы хотим спрятать:
:; @ECHO() { :;}; CLS() { clear; }; CHCP() { ENC="$1"; }Следующее, чему надо научиться — выводить текст в указанной в батнике кодировке. Это просто:
:; ECHO.() { echo $@ | iconv -f "CP$ENC"; }Если теперь сделать файл запускаемым и добавить впереди #!/bin/bash (и пренебречь тем, что cmd.exe успевает ругнуться на эту строку до очистки экрана), у нас получится следующий файл, работающий во всех трёх операционках (кодировка должна быть
#!/bin/bash
:; @ECHO() { :;}; CLS() { clear; }; CHCP() { ENC="$1"; }
@ECHO off
CHCP 1251
CLS
:; ECHO.() { echo $@ | iconv -f "CP$ENC"; }
ECHO. Всем приветА вот дальше сложнее. Само меню организовано в оригинале так:
SET /p opt=Введите цифру:
IF %opt%==1 GOTO dns_auto
IF %opt%==2 GOTO dnschange
IF %opt%==3 GOTO exitВ «баше» goto (переход к метке) отсутствует и это проблема. Аналогов тоже нет. Единственное, что тут можно сделать — использовать вызов функций, но синтаксис в «бате» и «баше» сильно различается.
Как быть?
:; SET() { shift; ECHO. -n "${@#*=}"; read -n1 "${1%%=*}"; echo; }
:; CALL() { eval "$(sed 's/%\(.*\)%/$\1/g' <<< "$1")" 2>&-; }
SET /p opt=Введите цифру:
CALL :menu_%opt%Первая функция парсит команду SET выводит строку после равно, ждёт ввода и записывает значение в имя переменной, указанной слева от равно.
Вторая — заменяет CALL. В ней %variable% заменяется на $variable, выполняется подстановка переменной и получившееся имя выполняется как команда или функция «баша». Конструкция 2>&- нужна, чтобы избежать вывода ошибки в ситуации, если пользователь введёт
Теперь надо
:function_1
ECHO это якобы функция
GOTO :EOFВ «баше» то же самое могло бы выглядеть, например, так:
function_1() {
echo это функция
}Как это объединить? Я придумал следующий подход:
:; GOTO() { :; };
GOTO ;#start
:function_1 (){
ECHO. это [якобы] функция
GOTO :EOF
:; }
:;#startВ первой строке определяется пустая функция для «баша», потому что эта часть синтаксиса
Ниже идёт строка GOTO ;#start. С точки зрения «баша» тут две конструкции — GOTO, которая вызывает определённую выше пустую функцию и #start — строка комментария, так как в«баше» с «решётки» начинаются комментарии.
В «батнике» же эта же строка будет означать переход к метке с именем ;#start. Этот переход нужен нам, чтобы «обогнуть» строки, которые определены ниже, иначе cmd.exe начнёт сразу их выполнять, а нам этого не нужно. Их нельзя «спрятать» ниже основной программы, так как в «баше» функции должны определены раньше их вызова.
Что происходит дальше? С точки зрения cmd.exe ниже расположена метка :function_1 (как показывают мои эксперименты, часть после пробела просто отбрасывается), потом тело функции, команда её завершения GOTO :EOF и метка с именем :;}.
С точки зрения «баша» там определяется функция с именем :function_1, ниже идут вызовы уже определённых мною функций ECHO. и GOTO, а ещё ниже — уже знакомая нам пустая команда : и фигурная скобка, завершающая тело функции.
Соединяем всё вместе и получается следующая программа:
#!/bin/bash
:; @ECHO() { :;}; CLS() { clear; }; CHCP() { ENC="$1"; }
@ECHO off
CHCP 1251
CLS
:; GOTO() { :; };
:; ECHO.() { echo $@ | iconv -f "CP$ENC"; }; PAUSE() { read; };
:; SET() { shift; ECHO. -n "${@#*=}"; read "${1%%=*}"; }
:; CALL() { eval "$(sed 's/%\(.*\)%/$\1/g' <<< "$1")" 2>&-; }
GOTO ;#start
:menu_1 (){
ECHO. Пункт первый
GOTO :EOF
:; }
:menu_2 (){
ECHO. Пункт второй
GOTO :EOF
:; }
:;#start
SET /p opt=Введите цифру:
CALL :menu_%opt%Мой оппонент признал, что в споре я победил.
Uptime в Windows (на CMD)
Я заморочился и написал uptime на чистом CMD, раз уж на PowerShell уже до меня сделали. CMD — ужасный язык программирования!
Это я после Турции до программирования дорвался. Посмотреть что получилось.
@ECHO OFF
REM Евгений Степанищев //bolknote.ru 2011/04/09
REM Здесь CALL нельзя использовать, иначе хилый CMD не выдержит и лопнет —
REM подстановки переменных через %var% и !var! хватать перестанет
FOR /F "usebackq tokens=3" %%i ^
IN (`net stats srv ^| findstr /R "[0-9][0-9]\.[0-9][0-9]\.[0-9][0-9]*"`) DO SET DATE0=%%i
SET YEAR0=%DATE0:~6%
SET MONTH0=%DATE0:~3,2%
SET DAY0=%DATE0:~0,2%
SET YEAR1=%DATE:~6%
SET MONTH1=%DATE:~3,2%
SET DAY1=%DATE:~0,2%
REM Избавляемся от ведущих нулей, иначе CMD будет считать
REM числа восьмеричными
IF %DAY0:~0,1% == 0 SET DAY0=%DAY0:~1%
IF %MONTH0:~0,1% == 0 SET MONTH0=%MONTH0:~1%
IF %DAY1:~0,1% == 0 SET DAY1=%DAY1:~1%
IF %MONTH1:~0,1% == 0 SET MONTH1=%MONTH1:~1%
SET /A SUM=%DAY1%-%DAY0%
SETLOCAL ENABLEDELAYEDEXPANSION
IF %YEAR1% == %YEAR0% (
SET /A END=%MONTH1%-1
FOR /L %%M IN (%MONTH0%,1,!END!) DO (
CALL :getMonthDays %%M %YEAR0%
SET /A SUM+=!DAYS!
)
) ELSE (
REM 02.2010 -> +10 months
FOR /L %%M IN (%MONTH0%,1,12) DO (
CALL :getMonthDays %%M %YEAR0%
SET /A SUM+=!DAYS!
)
REM 02.2011 -> +2 months
FOR /L %%M IN (1,1,%MONTH1%) DO (
CALL :getMonthDays %%M %YEAR0%
SET /A SUM+=!DAYS!
)
REM Full years
SET /A START=%YEAR0%+1
SET /A STOP=%YEAR1%-1
FOR /L %%Y IN (!START!,1,!STOP!) DO (
CALL :isLeapYear %%Y
IF !FEB! == 29 (
SET /A SUM+=366
) ELSE (
SET /A SUM+=365
)
)
)
ECHO %SUM%
GOTO :EOF
REM ====== Функции ==========
REM Високосный ли год?
REM isLeapYear <год>
:isLeapYear
SET /A FEB=(%1/400)*400
IF %FEB% == %1 (
SET FEB=29
GOTO :EOF
)
SET /A FEB=(%1/100)*100
IF %FEB% == %1 (
SET FEB=28
GOTO :EOF
)
SET /A FEB=(%1/4)*4
IF %FEB% == %1 (
SET FEB=29
GOTO :EOF
)
SET FEB=28
GOTO :EOF
REM Количество дней в месяце
REM getMonthDays <месяц> <год>
:getMonthDays
CALL :isLeapYear %2
IF %1 == 2 (
SET DAYS=%FEB%
GOTO :EOF
)
IF /I %1 LSS 8 (
SET DAYS=30
SET /A DAYS+="%1&1"
) ELSE (
SET DAYS=31
SET /A DAYS-="%1&1"
)
GOTO :EOF
Бесполезное: переводим строку в десятичные коды
Недавно решил попристальнее посмотреть что нового появилось в CMD (язык пакетных файлов Windows) со времён Windows NT. Написал небольшую программу, которая переводит строку в десятичные коды. Мне показалось, что CMD стал ещё более нелогичным чем был. Например, я так и не догадался как решить все проблемы с кавычками. Какое бы решение я не придумывал для того, чтобы проверить входной параметр, я тут же придумывал решение для того, чтобы разрушить код.
В итоге, исключить в программе влияние кавычек я не смог. Если
@ECHO OFF
REM BOLK
REM Вспомогательная процедура для прохождения по массиву
IF NOT "%2"=="" GOTO :LINE
SETLOCAL
REM Входной параметр
SET LINE="%~1"
ECHO In: %LINE%
REM Сюда попадёт результат из ONE
SET OUT=""
REM Переменная останова вспомогательной процедуры
SET LINEBREAK=0
REM Сюда мы поместим результат и выведем его
SET RESULT=
CALL :ONE
REM ECHO Out: %OUT%
REM GOTO :EOF
REM Цикл прохода для DEBUG, который даст нам кучу переменных с шестнадцатеричными
REM значениями в каждой
(ECHO E100 %OUT% 0 && ECHO D100 && ECHO Q) | ^
FOR /F "usebackq tokens=2-15 skip=2 delims=- " %%a IN (`DEBUG`) DO ^
@CALL %0 %%a %%b %%c %%d %%e %%f %%g %%h %%i %%j %%k %%l %%m %%n
GOTO :EOF
: Процедура осторожного разбора входной строки, разбираем так,
: чтобы спецсимволы не попали в код
:ONE
SET L="%LINE:~-2,1%"
REM Это для DEBUG — нужно чтобы заэкранировалась кавычка
IF ^%L:~1,-1%==^" (
SET OUT="""%OUT:~1,-1%
) ELSE (
SET OUT="%L:~1,-1%%OUT:~1,-1%"
)
SET LINE="%LINE:~1,-2%"
IF NOT %LINE%=="" GOTO ONE
GOTO :EOF
: Процедура для прохода по массиву. Так как вызывается через pipe,
: тормозить придётся самостоятельно — родительский процесс ничего отсюда
: не получает
:LINE
IF %LINEBREAK%==1 GOTO :EOF
IF "%1"=="00" (SET LINEBREAK=1 && ECHO %RESULT% && GOTO :EOF)
CALL :CHAR %1
SHIFT
GOTO LINE
: Победа разума над CMD — преобразование hex->dec
:CHAR
SET /A RES=0x%1
IF DEFINED RESULT (SET RESULT=%RESULT% %RES%) ELSE SET RESULT=%RES%