Pojedynczy skrypt do uruchomienia zarówno w systemie Windows, jak i Linux Bash?

96

Czy można napisać pojedynczy plik skryptu, który będzie wykonywany zarówno w systemie Windows (traktowany jako .bat), jak i Linux (przez Bash)?

Znam podstawową składnię obu, ale nie rozgryzłem. Prawdopodobnie może wykorzystać niejasną składnię Basha lub usterkę procesora wsadowego systemu Windows.

Polecenie do wykonania może składać się z jednej linii do wykonania innego skryptu.

Motywacją jest posiadanie tylko jednego polecenia rozruchu aplikacji dla systemu Windows i Linux.

Aktualizacja: Potrzeba "natywnego" skryptu powłoki systemu polega na tym, że musi on wybrać odpowiednią wersję interpretera, dostosować się do pewnych dobrze znanych zmiennych środowiskowych itp. Instalowanie dodatkowych środowisk, takich jak CygWin, nie jest preferowane - chciałbym zachować koncepcję " pobierz i uruchom ”.

Jedynym innym językiem do rozważenia w systemie Windows jest Windows Scripting Host - WSH, który jest domyślnie ustawiony od 98.

Ondra Žižka
źródło
9
Zajrzyj do Perla lub Pythona. Są to języki skryptowe dostępne na obu platformach.
a_horse_with_no_name
groovy, python, ruby, ktoś? stackoverflow.com/questions/257730/…
Kalpesh Soni
Groovy byłoby wspaniale mieć domyślnie we wszystkich systemach, potraktowałbym to jako język skryptów powłoki. Może kiedyś :)
Ondra Žižka
1
Zobacz też: github.com/BYVoid/Batsh
Dave Jarvis
2
Atrakcyjnym przykładem użycia tego są samouczki - aby móc po prostu powiedzieć ludziom „uruchom ten mały skrypt” bez konieczności wyjaśniania, że ​​„jeśli jesteś w systemie Windows, użyj tego małego skryptu, ale jeśli używasz systemu Linux lub Mac, spróbuj tego zamiast tego, czego właściwie nie testowałem, ponieważ używam systemu Windows ”. Niestety system Windows nie ma podstawowych poleceń podobnych do uniksowych, takich jak cpwbudowane, lub odwrotnie, więc napisanie dwóch oddzielnych skryptów może być lepsze z pedagogiki niż pokazane tutaj zaawansowane techniki.
Qwertie

Odpowiedzi:

92

To, co zrobiłem, to użycie składni etykiety cmd jako znacznika komentarza . Znak etykiety, dwukropek ( :), jest równoważny truew większości powłok POSIXish. Jeśli natychmiast po znaku etykiety pojawi się inny znak, którego nie można użyć w a GOTO, to komentowanie cmdskryptu nie powinno wpływać na cmdkod.

Hack polega na umieszczeniu linii kodu po sekwencji znaków „ :;”. Jeśli piszesz głównie skrypty jednowierszowe lub, jak może być, możesz napisać jedną linię shdla wielu wierszy cmd, poniższe mogą być w porządku. Nie zapominaj, że każde użycie $?musi nastąpić przed następnym dwukropkiem, :ponieważ :resetuje się $?do 0.

:; echo "Hi, I’m ${SHELL}."; exit $?
@ECHO OFF
ECHO I'm %COMSPEC%

Bardzo wymyślny przykład ochrony $?:

:; false; ret=$?
:; [ ${ret} = 0 ] || { echo "Program failed with code ${ret}." >&2; exit 1; }
:; exit
ECHO CMD code.

Innym pomysłem na pominięcie cmdkodu jest użycie heredoców , które shtraktują cmdkod jako nieużywany ciąg i cmdinterpretują go. W tym przypadku upewniamy się, że separator naszego heredoc jest zarówno cytowany (aby nie shwykonywać jakiejkolwiek interpretacji jego zawartości podczas uruchamiania z sh), jak i zaczyna się od :tak, żecmd przeskakiwał go jak każdy inny wiersz zaczynający się od :.

:; echo "I am ${SHELL}"
:<<"::CMDLITERAL"
ECHO I am %COMSPEC%
::CMDLITERAL
:; echo "And ${SHELL} is back!"
:; exit
ECHO And back to %COMSPEC%

W zależności od Twoich potrzeb lub stylu kodowania przeplot cmdi shkod mogą mieć sens lub nie. Jedną z metod wykonywania takiego przeplotu jest użycie heredoców. Można to jednak rozszerzyć za pomocą GOTOtechniki :

:<<"::CMDLITERAL"
@ECHO OFF
GOTO :CMDSCRIPT
::CMDLITERAL

echo "I can write free-form ${SHELL} now!"
if :; then
  echo "This makes conditional constructs so much easier because"
  echo "they can now span multiple lines."
fi
exit $?

:CMDSCRIPT
ECHO Welcome to %COMSPEC%

Komentarze uniwersalne można oczywiście wykonać za pomocą sekwencji znaków : #lub :;#. Spacja lub średnik są konieczne, ponieważ shrozważa# się je za część nazwy polecenia, jeśli nie jest pierwszym znakiem identyfikatora. Na przykład możesz chcieć napisać uniwersalne komentarze w pierwszych wierszach pliku przed użyciem GOTOmetody do podzielenia kodu. Następnie możesz poinformować czytelnika, dlaczego Twój skrypt jest napisany tak dziwnie:

: # This is a special script which intermixes both sh
: # and cmd code. It is written this way because it is
: # used in system() shell-outs directly in otherwise
: # portable code. See /programming/17510688
: # for details.
:; echo "This is ${SHELL}"; exit
@ECHO OFF
ECHO This is %COMSPEC%

Stąd kilka pomysłów i sposobów tworzenia shi cmdkompatybilnych skryptów bez poważnych skutków ubocznych, o ile wiem (i bez cmdwyjścia '#' is not recognized as an internal or external command, operable program or batch file.).

binki
źródło
5
Zwróć uwagę, że wymaga to .cmdrozszerzenia skryptu , w przeciwnym razie nie będzie działać w systemie Windows. Czy jest jakieś obejście?
jviotti
1
Jeśli piszesz pliki wsadowe, polecam po prostu nazwać je .cmdi oznaczyć jako wykonywalne. W ten sposób wykonanie skryptu z domyślnej powłoki na Windows lub unix będzie działać (testowane tylko z bash). Ponieważ nie mogę wymyślić sposobu na włączenie shebanga bez głośnego narzekania cmd, musisz zawsze wywoływać skrypt przez powłokę. To znaczy , upewnij się, że używasz execlp()lub execvp()(myślę, że system()robi to „we właściwy” sposób dla Ciebie) zamiast execl()lub execv()(te ostatnie funkcje będą działać tylko w skryptach z shebangami, ponieważ trafiają bardziej bezpośrednio do systemu operacyjnego).
binki
Pominąłem dyskusję na temat rozszerzeń plików, ponieważ pisałem swój przenośny kod w shelloutach (np. <Exec/>MSBuild Task ), a nie jako niezależne pliki wsadowe. Może jeśli będę miał jakiś czas w przyszłości, zaktualizuję odpowiedź o informacje w tych komentarzach…
binki
23

możesz spróbować tego:

#|| goto :batch_part
 echo $PATH
#exiting the bash part
exit
:batch_part
 echo %PATH%

Prawdopodobnie będziesz musiał użyć /r/njako nowej linii zamiast stylu uniksowego.Jeśli pamiętam poprawne, nowa linia uniksowa nie jest interpretowana jako nowa linia przez skrypty Innym .batsposobem jest utworzenie #.exepliku w ścieżce, który nic nie robi podobnie jak moja odpowiedź tutaj: czy można osadzić i uruchomić VBScript w pliku wsadowym bez użycia pliku tymczasowego?

EDYTOWAĆ

Odpowiedź binki jest prawie idealna, ale nadal można ją poprawić:

:<<BATCH
    @echo off
    echo %PATH%
    exit /b
BATCH

echo $PATH

Używa ponownie :sztuczki i komentarza wieloliniowego. Wygląda na to, że cmd.exe (przynajmniej w systemie Windows10) działa bez problemów z EOL w stylu unix, więc upewnij się, że twój skrypt jest konwertowany do formatu linux. (to samo podejście zostało zastosowane tutaj i tutaj ). Chociaż użycie shebang nadal da zbędne wyjście ...

npocmaka
źródło
3
/r/nna końcu linii spowoduje wiele bólu głowy w skrypcie bash, chyba że zakończysz każdą linię znakiem #(tj. #/r/n) - w ten sposób \rzostanie potraktowany jako część komentarza i zignorowany.
Gordon Davisson,
2
Właściwie uważam, że # 2>NUL & ECHO Welcome to %COMSPEC%.udaje mu się ukryć błąd dotyczący #polecenia, którego nie znaleziono cmd. Ta technika jest przydatna, gdy musisz napisać samodzielną linię, która będzie wykonywana tylko przez cmd(np. W a Makefile).
binki
11

Chciałem skomentować, ale w tej chwili mogę tylko dodać odpowiedź.

Podane techniki są doskonałe i również z nich korzystam.

Trudno jest zachować plik, który zawiera dwa rodzaje podziałów wierszy, czyli /nczęść bash i/r/n dla części okna. Większość edytorów próbuje wymusić wspólny schemat łamania linii, zgadując, jaki rodzaj pliku edytujesz. Ponadto większość metod przesyłania pliku przez Internet (szczególnie jako plik tekstowy lub skrypt) usuwa podziały wierszy, więc można zacząć od jednego rodzaju podziału wiersza, a skończyć na drugim. Jeśli przyjąłeś założenia dotyczące łamania linii, a następnie przekazałeś swój skrypt komuś innemu, może się okazać, że to nie zadziała.

Innym problemem są systemy plików zamontowane w sieci (lub dyski CD), które są współdzielone między różnymi typami systemów (szczególnie tam, gdzie nie można kontrolować oprogramowania dostępnego dla użytkownika).

Dlatego należy używać podziału wiersza DOS, /r/na także chronić skrypt bash przed DOS /r, umieszczając komentarz na końcu każdej linii ( #). Nie można również używać kontynuacji linii w bash, ponieważ/r spowoduje to ich zerwanie.

W ten sposób ktokolwiek używa skryptu iw jakimkolwiek środowisku, będzie on działał.

Używam tej metody w połączeniu z tworzeniem przenośnych plików Makefile!

Brian Tompsett - 汤 莱恩
źródło
6

Poniższe działa dla mnie bez żadnych błędów lub komunikatów o błędach z Bash 4 i Windows 10, w przeciwieństwie do powyższych odpowiedzi. Nazywam plik "cokolwiek.cmd", robię, chmod +xżeby był wykonywalny w Linuksie i sprawiam, że ma końcówki linii unix ( dos2unix), aby bash był cichy.

:; if [ -z 0 ]; then
  @echo off
  goto :WINDOWS
fi

if [ -z "$2" ]; then
  echo "usage: $0 <firstArg> <secondArg>"
  exit 1
fi

# bash stuff
exit

:WINDOWS
if [%2]==[] (
  SETLOCAL enabledelayedexpansion
  set usage="usage: %0 <firstArg> <secondArg>"
  @echo !usage:"=!
  exit /b 1
)

:: windows stuff
Remik Ziemliński
źródło
3

Możesz udostępniać zmienne:

:;SET() { eval $1; }

SET var=value

:;echo $var
:;exit
ECHO %var%
Velkan
źródło
2

Istnieje kilka sposobów wykonywania różnych poleceń w tym samym skrypcie bashi za cmdpomocą tego samego skryptu.

cmdzignoruje wiersze zaczynające się od :;, jak wspomniano w innych odpowiedziach. Zignoruje również następną linię, jeśli bieżąca linia kończy się poleceniem rem ^, ponieważ ^znak uniknie końca linii, a następna linia będzie traktowana jako komentarz przez rem.

Jeśli chodzi o bashignorowanie cmdwierszy, istnieje wiele sposobów. Wyliczyłem kilka sposobów, aby to zrobić bez łamania cmdpoleceń:

Nieistniejące #polecenie (niezalecane)

Jeśli po uruchomieniu skryptu nie jest #dostępne żadne polecenie cmd, możemy to zrobić:

# 2>nul & echo Hello cmd! & rem ^
echo 'Hello bash!' #

#Charakter na początku cmdmarek linii bashtraktują tę linię jako komentarz.

#Znaków na końcu bashlinii jest używany do ustosunkowania się, \rznak, jak Brian Tompsett wskazał w swojej odpowiedzi . Bez tego bashzgłosi błąd, jeśli plik ma \r\nzakończenia linii, wymagane przez cmd.

W ten # 2>nulsposób oszukujemy, cmdaby zignorować błąd jakiegoś nieistniejącego #polecenia, jednocześnie wykonując następujące polecenie.

Nie używaj tego rozwiązania, jeśli #polecenie jest dostępne na stronie PATHlub jeśli nie masz kontroli nad poleceniami dostępnymi dla cmd.


Używając, echoaby zignorować #włączony znakcmd

Możemy użyć echoz przekierowanym wyjściem do wstawiania cmdpoleceń w bashzakomentowanym obszarze:

echo >/dev/null # >nul & echo Hello cmd! & rem ^
echo 'Hello bash!' #

Ponieważ #znak nie ma specjalnego znaczenia cmd, jest traktowany jako część tekstu do echo. Wszystko, co musieliśmy zrobić, to przekierować wyjście echopolecenia i wstawić po nim inne polecenia.


Pusty #.batplik

echo >/dev/null # 1>nul 2> #.bat
# & echo Hello cmd! & del #.bat & rem ^
echo 'Hello bash!' #

echo >/dev/null # 1>nul 2> #.batLinia tworzy pusty #.batplik while on cmd(lub zastępuje istniejące #.bat, jeśli w ogóle), a nie robi nic na while bash.

Plik ten będzie używany przez cmdlinię (S), który następuje, nawet jeśli jest jakaś inna #komenda na PATH.

del #.batPolecenia na cmdkodzie swoistej wobec usuwa plik, który został utworzony. Musisz to zrobić tylko w ostatniej cmdlinii.

Nie używaj tego rozwiązania, jeśli #.batplik może znajdować się w Twoim bieżącym katalogu roboczym, ponieważ zostanie on usunięty.


Zalecane: używanie dokumentu here do ignorowania cmdpoleceń nabash

:; echo 'Hello bash!';<<:
echo Hello cmd! & ^
:

Umieszczając ^znak na końcu cmdlinii, unikamy podziału wiersza i używając :jako separatora dokumentu w tym miejscu, zawartość linii separatora nie będzie miała wpływu na cmd. W ten sposób cmdwykona swoją linię dopiero po jej zakończeniu :, zachowując się tak samo jak bash.

Jeśli chcesz mieć wiele linii na obu platformach i wykonywać je tylko na końcu bloku, możesz to zrobić:

:;( #
  :;  echo 'Hello'  #
  :;  echo 'bash!'  #
:; );<<'here-document delimiter'
(
      echo Hello
      echo cmd!
) & rem ^
here-document delimiter

Dopóki nie ma cmddokładnej linii here-document delimiter, to rozwiązanie powinno działać. Możesz zmienić here-document delimiterna dowolny inny tekst.


We wszystkich przedstawionych rozwiązaniach polecenia będą wykonywane dopiero po ostatniej linii , dzięki czemu ich zachowanie będzie spójne, jeśli zrobią to samo na obu platformach.

Te rozwiązania muszą zostać zapisane w plikach z \r\npodziałem wiersza, w przeciwnym razie nie będą działać cmd.

Tex Killer
źródło
Lepiej późno niż wcale ... to pomogło mi bardziej niż inne odpowiedzi. Dzięki!!
A-Diddy
2

Poprzednie odpowiedzi wydają się obejmować prawie wszystkie opcje i bardzo mi pomogły. Dołączam tę odpowiedź tutaj, aby zademonstrować mechanizm, którego użyłem do włączenia zarówno skryptu Bash, jak i skryptu CMD systemu Windows w tym samym pliku.

LinuxWindowsScript.bat

echo >/dev/null # >nul & GOTO WINDOWS & rem ^
echo 'Processing for Linux'

# ***********************************************************
# * NOTE: If you modify this content, be sure to remove carriage returns (\r) 
# *       from the Linux part and leave them in together with the line feeds 
# *       (\n) for the Windows part. In summary:
# *           New lines in Linux: \n
# *           New lines in Windows: \r\n 
# ***********************************************************

# Do Linux Bash commands here... for example:
StartDir="$(pwd)"

# Then, when all Linux commands are complete, end the script with 'exit'...
exit 0

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

:WINDOWS
echo "Processing for Windows"

REM Do Windows CMD commands here... for example:
SET StartDir=%cd%

REM Then, when all Windows commands are complete... the script is done.

Podsumowanie

W systemie Linux

Pierwsza linia ( echo >/dev/null # >nul & GOTO WINDOWS & rem ^) zostanie zignorowana, a skrypt będzie przepływał przez każdą następną bezpośrednio po niej linię, aż exit 0polecenie zostanie wykonane. Po exit 0osiągnięciu wykonywania skryptu zakończy się, ignorując polecenia systemu Windows poniżej.

W systemie Windows

Pierwsza linia wykona GOTO WINDOWSpolecenie, pomijając polecenia Linuksa bezpośrednio następujące po nim i kontynuując wykonywanie w :WINDOWSlinii.

Usuwanie zwrotów karetki w systemie Windows

Ponieważ edytowałem ten plik w systemie Windows, musiałem systematycznie usuwać znaki powrotu karetki (\ r) z poleceń Linuksa, bo inaczej otrzymałem nieprawidłowe wyniki podczas uruchamiania części Bash. Aby to zrobić, otworzyłem plik w Notepad ++ i wykonałem następujące czynności:

  1. Włącz opcję do wyświetlania znaków końca wiersza ( View> Show Symbol> Show End of Line). Powroty karetki będą wtedy wyświetlane jako CRznaki.

  2. Wykonaj polecenie Znajdź i zamień ( Search> Replace...) i zaznacz Extended (\n, \r, \t, \0, \x...)opcję.

  3. Wpisz \rw Find what :polu i puste na zewnątrz Replace with :pola, więc nie ma nic w nim.

  4. Zaczynając od góry pliku, klikaj Replaceprzycisk, aż wszystkie znaki powrotu karetki ( CR) zostaną usunięte z górnej części linuksowej. Pamiętaj, aby zostawić znaki powrotu karetki ( CR) dla części Windows.

Wynik powinien być taki, że każde polecenie Linuksa kończy się tylko znakiem wysuwu wiersza ( LF), a każde polecenie systemu Windows znakiem powrotu karetki i wysuwu wiersza ( CR LF).

A-Diddy
źródło
1

Używam tej techniki do tworzenia uruchamialnych plików jar. Ponieważ plik jar / zip zaczyna się w nagłówku zip, mogę umieścić uniwersalny skrypt, aby uruchomić ten plik na górze:

#!/usr/bin/env sh
@ 2>/dev/null # 2>nul & echo off
:; alias ::=''
:: exec java -jar $JAVA_OPTS "$0" "$@"
:: exit
java -jar %JAVA_OPTS% "%~dpnx0" %*
exit /B
  • Pierwsza linia odbija się echem w cmd i nie drukuje niczego na sh. Dzieje się tak, ponieważ @in sh zgłasza błąd, który jest przesyłany potokiem do, /dev/nulla następnie rozpoczyna się komentarz. W cmd potok /dev/nullkończy się niepowodzeniem, ponieważ plik nie jest rozpoznawany w systemie Windows, ale ponieważ system Windows nie wykrywa #jako komentarza, błąd jest przesyłany potokiem nul. Następnie wyłącza echo. Ponieważ cała linia jest poprzedzona @znakiem, to nie dostaje printet w cmd.
  • Drugi definiuje ::, który rozpoczyna komentarz w cmd, do noop w sh. Ma to tę zaletę, że ::nie resetuje się $?do 0. Używa :;sztuczki „ jest etykietą”.
  • Teraz mogę poprzedzać polecenia sh ::i są one ignorowane w cmd
  • Po :: exitzakończeniu skryptu sh i mogę pisać polecenia cmd
  • Tylko pierwsza linia (shebang) jest problematyczna w cmd, ponieważ zostanie wydrukowana command not found. Musisz sam zdecydować, czy tego potrzebujesz, czy nie.
LolHens
źródło
0

Potrzebowałem tego dla niektórych moich skryptów instalacyjnych pakietu Python. Większość rzeczy między plikiem sh i bat jest taka sama, ale kilka rzeczy, takich jak obsługa błędów, jest różnych. Można to zrobić w następujący sposób:

common.inc
----------
common statement1
common statement2

Następnie wywołujesz to ze skryptu bash:

linux.sh
--------
# do linux specific stuff
...
# call common code
source common.inc

Plik wsadowy systemu Windows wygląda następująco:

windows.bat
-----------
REM do windows specific things
...
# call common code
call common.inc
Shital Shah
źródło
-6

Istnieją niezależne od platformy narzędzia do budowania, takie jak Ant lub Maven ze składnią xml (oparte na Javie). Możesz więc przepisać wszystkie swoje skrypty w Ant lub Maven i uruchomić je niezależnie od typu systemu operacyjnego. Możesz też po prostu utworzyć skrypt opakowujący Ant, który przeanalizuje typ systemu operacyjnego i uruchomi odpowiedni skrypt bat lub bash.

monitor
źródło
5
Wydaje się, że jest to rażące nadużycie tych narzędzi. Jeśli chcesz uniknąć rzeczywistego pytania (działającego w natywnej powłoce), powinieneś zasugerować uniwersalny język skryptowy, taki jak python, a nie narzędzie do budowania, które może być nadużywane jako zamiennik właściwego języka skryptowego.
ArtOfWarfare