Scegli l'interprete dopo l'avvio dello script, ad esempio if / else all'interno di hashbang


16

Esiste un modo per scegliere in modo dinamico l'interprete che esegue uno script? Ho uno script che sto eseguendo su due sistemi diversi e l'interprete che voglio usare si trova in posizioni diverse sui due sistemi. Quello che alla fine devo fare è cambiare la linea di hashbang ogni volta che passo. Vorrei fare qualcosa che è l' equivalente logico di questo (mi rendo conto che questo costrutto esatto è impossibile):

if running on system A:
    #!/path/to/python/on/systemA
elif running on system B:
    #!/path/on/systemB

#Rest of script goes here

O ancora meglio sarebbe questo, in modo che tenti di usare il primo interprete, e se non lo trova usa il secondo:

try:
    #!/path/to/python/on/systemA
except: 
    #!path/on/systemB

#Rest of script goes here

Ovviamente, posso invece eseguirlo come /path/to/python/on/systemA myscript.py o in /path/on/systemB myscript.py base a dove mi trovo, ma in realtà ho uno script wrapper che si avvia myscript.py, quindi vorrei specificare il percorso dell'interprete Python a livello di programmazione piuttosto che a mano.


3
passare il "resto dello script" come file all'interprete senza shebang e usare la ifcondizione non è un'opzione per te? tipo,if something; then /bin/sh restofscript.sh elif...
mazs

È un'opzione, l'ho anche considerata, ma un po 'più caotica di quanto vorrei. Poiché la logica nella linea hashbang è impossibile, penso che seguirò davvero quella strada.
dkv,

Mi piace l'ampia gamma di risposte diverse che questa domanda ha generato.
Oskar Skog,

Risposte:


27

No, non funzionerà. I due caratteri #!devono assolutamente essere i primi due caratteri nel file (come specificheresti comunque cosa ha interpretato l'istruzione if?). Ciò costituisce il "numero magico" che la exec()famiglia di funzioni rileva quando determinano se un file che stanno per eseguire è uno script (che necessita di un interprete) o un file binario (che non lo fa).

Il formato della linea Shebang è piuttosto rigoroso. Deve avere un percorso assoluto verso un interprete e al massimo un argomento ad esso.

Quello che puoi fare è usare env:

#!/usr/bin/env interpreter

Ora, il percorso envè di solito /usr/bin/env , ma tecnicamente non è una garanzia.

Questo consente di regolare la PATHvariabile d'ambiente su ogni sistema in modo che interpreter(sia esso bash, pythono perlo quello che avete) si trova.

Un aspetto negativo di questo approccio è che sarà impossibile passare in modo portabile un argomento all'interprete.

Ciò significa che

#!/usr/bin/env awk -f

e

#!/usr/bin/env sed -f

è improbabile che funzioni su alcuni sistemi.

Un altro ovvio approccio consiste nell'utilizzare gli autotools GNU (o un sistema di templating più semplice) per trovare l'interprete e posizionare il percorso corretto nel file in un ./configurepassaggio, che verrebbe eseguito al momento dell'installazione dello script su ciascun sistema.

Si potrebbe anche ricorrere all'esecuzione dello script con un interprete esplicito, ma è ovviamente quello che stai cercando di evitare:

$ sed -f script.sed

Bene, mi rendo conto che #!deve venire all'inizio, poiché non è la shell che elabora quella linea. Mi chiedevo se c'è un modo per mettere la logica all'interno della linea hashbang che sarebbe equivalente a if / else. Speravo anche di evitare di scherzare con il mio, PATHma immagino che quelle siano le mie uniche opzioni.
dkv,

1
Quando si utilizza #!/usr/bin/awk, è possibile fornire esattamente un argomento, ad esempio #!/usr/bin/awk -f. Se il binario a cui stai puntando è env, l'argomento è il binario che stai chiedendo envdi cercare, come in #!/usr/bin/env awk.
DopeGhoti,

2
@dkv Non lo è. Utilizza un interprete con due argomenti e può funzionare su alcuni sistemi, ma sicuramente non su tutti.
Kusalananda

3
@dkv su Linux funziona /usr/bin/envcon il singolo argomento awk -f.
ilkkachu,

1
@Kusalananda, no, questo era il punto. Se hai uno script chiamato foo.awkcon la linea hashbang #!/usr/bin/env awk -fe lo chiami con ./foo.awkallora, su Linux, ciò che envvede sono i due parametri awk -fe ./foo.awk. In realtà va cercando /usr/bin/awk -f(ecc.) Con uno spazio.
ilkkachu,

27

È sempre possibile creare uno script wrapper per trovare l'interprete corretto per il programma effettivo:

#!/bin/bash
if something ; then
    interpreter=this
    script=/some/path/to/program.real
    flags=()
else
    interpreter=that
    script=/other/path/to/program.real
    flags=(-x -y)
fi
exec "$interpreter" "${flags[@]}" "$script" "$@"

Salvare il wrapper negli utenti PATHcome programe mettere da parte il programma effettivo o con un altro nome.

Ho usato #!/bin/bashl'hashbang a causa flagsdell'array. Se non hai bisogno di memorizzare un numero variabile di flag o simili e puoi farne a meno, lo script dovrebbe funzionare in modo portabile #!/bin/sh.


2
Ho visto exec "$interpreter" "${flags[@]}" "$script" "$@"anche usato per mantenere più pulito l'albero di processo. Propaga anche il codice di uscita.
rrauenza,

@rrauenza, ah sì, naturalmente con exec.
ilkkachu,

1
Non #!/bin/shsarebbe meglio invece di #!/bin/bash? Anche se /bin/shè un collegamento simbolico a una shell diversa, dovrebbe esistere sulla maggior parte (se non su tutti) i sistemi * nix, inoltre costringerebbe l'autore dello script a creare uno script portatile piuttosto che cadere nei bashismi.
Sergiy Kolodyazhnyy,

@SergiyKolodyazhnyy, eh, ho pensato di menzionarlo prima, ma poi no. L'array utilizzato flagsè una funzionalità non standard, ma è abbastanza utile per memorizzare un numero variabile di flag, quindi ho deciso di mantenerlo.
ilkkachu,

O l'uso / bin / sh e basta chiamare l'interprete direttamente in ogni ramo: script=/what/ever; something && exec this "$script" "$@"; exec that "$script" -x -y "$@". È inoltre possibile aggiungere il controllo degli errori per errori exec.
jrw32982 supporta Monica

11

Puoi anche scrivere un poliglotta (combina due lingue). / bin / sh è garantito per esistere.

Questo ha il rovescio della medaglia del codice brutto e forse alcuni /bin/shpotrebbero potenzialmente essere confusi. Ma può essere usato quando envnon esiste o esiste altrove rispetto a / usr / bin / env. Può anche essere usato se vuoi fare una selezione piuttosto elegante.

La prima parte dello script determina quale interprete usare quando eseguito con / bin / sh come interprete, ma viene ignorato quando eseguito dall'interprete corretto. Utilizzare execper impedire l'esecuzione della shell più della prima parte.

Esempio Python:

#!/bin/sh
'''
' 2>/dev/null
# Python thinks this is a string, docstring unfortunately.
# The shell has just tried running the <newline> program.
find_best_python ()
{
    for candidate in pypy3 pypy python3 python; do
        if [ -n "$(which $candidate)" ]; then
            echo $candidate
            return
        fi
    done
    echo "Can't find any Python" >/dev/stderr
    exit 1
}
interpreter="$(find_best_python)"   # Replace with something fancier.
# Run the rest of the script
exec "$interpreter" "$0" "$@"
'''

3
Penso di aver visto uno di questi prima, ma l'idea è ancora ugualmente terribile ... Ma, probabilmente, vorrai exec "$interpreter" "$0" "$@"ottenere il nome della sceneggiatura stessa anche per l'interprete reale. (E poi spero che nessuno abbia mentito durante l'installazione $0.)
Ilkkachu,

6
Scala in realtà ha il supporto per gli script poliglotta nella sua sintassi: se uno script Scala inizia con #!, Scala ignora tutto fino a una corrispondenza !#; questo ti permette di inserire codice di script arbitrariamente complesso in un linguaggio arbitrario e quindi execil motore di esecuzione di Scala con lo script.
Jörg W Mittag,

1
@ Jörg W Mittag: +1 per Scala
jrw32982 supporta Monica

2

Preferisco le risposte di Kusalananda e ilkkachu, ma ecco una risposta alternativa che fa più direttamente ciò che la domanda stava chiedendo, semplicemente perché era stata posta.

#!/usr/bin/ruby -e exec "non-existing-interpreter", ARGV[0] rescue exec "python", ARGV[0]

if True:
  print("hello world!")

Si noti che è possibile farlo solo quando l'interprete consente di scrivere codice nel primo argomento. Qui, -ee tutto il resto viene preso alla lettera come 1 argomento a ruby. Per quanto ne so, non puoi usare bash per il codice shebang, perché bash -crichiede che il codice sia in un argomento separato.

Ho provato a fare lo stesso con Python per il codice Shebang:

#!/usr/bin/python -cexec("import sys,os\ntry: os.execlp('non-existing-interpreter', 'non-existing-interpreter', sys.argv[1])\nexcept: os.execlp('ruby', 'ruby', sys.argv[1])")

if true
  puts "hello world!"
end

ma risulta troppo lungo e Linux (almeno sulla mia macchina) tronca lo shebang a 127 caratteri. Si prega di scusare l'uso di execper inserire nuove righe in quanto Python non consente di provare-esecuzioni o imports senza nuove righe.

Non sono sicuro di quanto sia portatile e non lo farei con il codice che si intende distribuire. Tuttavia, è fattibile. Forse qualcuno lo troverà utile per il debugging veloce e sporco o qualcosa del genere.


2

Anche se questo non seleziona l'interprete all'interno dello script della shell (lo seleziona per macchina) è un'alternativa più semplice se si ha accesso amministrativo a tutte le macchine su cui si sta tentando di eseguire lo script.

Creare un collegamento simbolico (o un collegamento fisico se desiderato) per puntare al percorso dell'interprete desiderato. Ad esempio, sul mio sistema perl e python sono in / usr / bin:

cd /bin
ln -s /usr/bin/perl perl
ln -s /usr/bin/python python

creerebbe un collegamento simbolico per consentire all'hashbang di risolvere per / bin / perl, ecc. Ciò preserva la capacità di passare parametri anche agli script.


1
+1 Questo è così semplice. Come notate, non risponde esattamente alla domanda, ma sembra fare esattamente ciò che l'OP vuole. Anche se immagino che l'utilizzo di env aggiri l'accesso root su ogni problema della macchina.
Joe,

0

Mi sono trovato di fronte a un problema simile come questo oggi ( python3indicando una versione di Python che era troppo vecchia su un sistema), e ho trovato un approccio un po 'diverso da quelli discussi qui: Usa la versione "sbagliata" di Python per bootstrap in quello "giusto". Il limite è che alcune versioni di Python devono essere raggiungibili in modo affidabile, ma che di solito possono essere raggiunte ad es #!/usr/bin/env python3.

Quindi quello che faccio è iniziare la mia sceneggiatura con:

#!/usr/bin/env python3
import sys
import os

# On one of our systems, python3 is pointing to python3.3
# which is too old for our purposes. 'Upgrade' if needed
if sys.version_info[1] < 4:
    for py_version in ['python3.7', 'python3.6', 'python3.5', 'python3.4']:
        try:
            os.execlp(py_version, py_version, *sys.argv)
        except:
            pass # Deliberately ignore errors, pick first available version

Quello che fa è:

  • Controllare la versione dell'interprete per alcuni criteri di accettazione
  • Se non è accettabile, consulta un elenco di versioni candidate e riesegui se stesso con il primo disponibile
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.