cmd.sh
Я тут случайно в спор ввязался — можно ли переписать бат-файл так, чтобы он запускался и выводил примерно одинаковый результат на трёх основных операционных системах — Виндоузе, Линуксе и МакОСи.
Речь шла о вполне определённом файле, там запрограммировано небольшое меню и в зависимости от выбранного пункта запускается та или иная, одинаковая для всех ОС, последовательность команд.
Мой собеседник мне не верил, когда я утверждал, что это вполне возможно, и требовал доказательств.
Что ж. Основная моя идея была в том, чтобы заменить команды батника на вызовы баш-функций, которые я спрячу в специальных конструкциях. В Виндоузе файл обработает обычный cmd.exe, на остальных операционках — bash.
То, что мне предстояло адаптировать начиналось вполне стандартно:
@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 успевает ругнуться на эту строку до очистки экрана), у нас получится следующий файл, работающий во всех трёх операционках (кодировка должна быть Windows-1251):
#!/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, во-вторвых, упростить это место, чтобы меньше было писать кода на «баше»:
:; 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>&- нужна, чтобы избежать вывода ошибки в ситуации, если пользователь введёт что-нибудь не то.
Теперь надо как-то научиться определять функции так, чтобы их нормально «видел» и cmd.exe, и «баш». В батнике функции — просто любое место программы, начинающееся с метки и заканчивающееся вызовом GOTO :EOF:
: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%
Мой оппонент признал, что в споре я победил.
Похожий опыт от одного интересного ленинградца:
Один бинарник на четыре системы
https://teletype.in/@alex0x08/full-cross
Мой оппонент специально оговорил, что так нельзя — основная программа должна быть одинакова, иначе неспортивно ) Но статья интересная, спасибо!