come scaricare un file usando solo bash e nient'altro (nessun ricciolo, wget, perl, ecc.)


40

Ho un minimo * nix senza testa che non ha utilità da riga di comando per il download di file (ad esempio nessun ricciolo, wget, ecc.). Ho solo bash.

Come posso scaricare un file?

Idealmente, vorrei una soluzione che potesse funzionare su una vasta gamma di * nix.


che ne dicigawk
Neil McGuigan,

Non riesco a ricordare ora se Gawk era disponibile, anche se mi piacerebbe vedere una soluzione basata su Gawk se ne hai una :)
Chris Snow

Risposte:


64

Se hai bash 2.04 o versioni successive con lo /dev/tcppseudo-dispositivo abilitato, puoi scaricare un file da bash stesso.

Incolla il seguente codice direttamente in una shell bash (non è necessario salvare il codice in un file per l'esecuzione):

function __wget() {
    : ${DEBUG:=0}
    local URL=$1
    local tag="Connection: close"
    local mark=0

    if [ -z "${URL}" ]; then
        printf "Usage: %s \"URL\" [e.g.: %s http://www.google.com/]" \
               "${FUNCNAME[0]}" "${FUNCNAME[0]}"
        return 1;
    fi
    read proto server path <<<$(echo ${URL//// })
    DOC=/${path// //}
    HOST=${server//:*}
    PORT=${server//*:}
    [[ x"${HOST}" == x"${PORT}" ]] && PORT=80
    [[ $DEBUG -eq 1 ]] && echo "HOST=$HOST"
    [[ $DEBUG -eq 1 ]] && echo "PORT=$PORT"
    [[ $DEBUG -eq 1 ]] && echo "DOC =$DOC"

    exec 3<>/dev/tcp/${HOST}/$PORT
    echo -en "GET ${DOC} HTTP/1.1\r\nHost: ${HOST}\r\n${tag}\r\n\r\n" >&3
    while read line; do
        [[ $mark -eq 1 ]] && echo $line
        if [[ "${line}" =~ "${tag}" ]]; then
            mark=1
        fi
    done <&3
    exec 3>&-
}

Quindi è possibile eseguirlo come dalla shell come segue:

__wget http://example.iana.org/

Fonte: la risposta di Moreaki che aggiorna e installa i pacchetti tramite la riga di comando di cygwin?

Aggiornamento: come menzionato nel commento, l'approccio sopra descritto è semplicistico:

  • la readvolontà cancella le barre rovesciate e lo spazio bianco iniziale.
  • Bash non può gestire molto bene i byte NUL, quindi i file binari sono fuori.
  • non quotato $linesarà glob.

8
Quindi hai risposto alla tua domanda nello stesso momento in cui l'hai fatta. È una macchina del tempo interessante che hai;)
Meer Borg,

11
@MeerBorg - quando fai una domanda, cerca la casella 'rispondi alla tua domanda' - blog.stackoverflow.com/2011/07/…
Chris Snow

@eestartup - Non credo che tu possa votare per la tua risposta. Posso spiegare il codice? Non ancora! Ma funziona su Cygwin.
Chris Snow,

3
Solo una nota: questo non funzionerà con alcune configurazioni di Bash. Credo che Debian configura questa funzionalità fuori dalla loro distribuzione di Bash.

1
Urgh, anche se questo è un bel trucco, può causare facilmente download corrotti. while readin questo modo elimina le barre rovesciate e i primi spazi bianchi e Bash non è in grado di gestire i byte NUL molto bene, quindi i file binari vengono rilasciati. E non quotato $lineglob ... Nulla di tutto ciò che vedo menzionato nella risposta.
ilkkachu,

19

Usa la lince.

È abbastanza comune per la maggior parte di Unix / Linux.

lynx -dump http://www.google.com

-dump: scarica il primo file su stdout ed esce

man lynx

O netcat:

/usr/bin/printf 'GET / \n' | nc www.google.com 80

O telnet:

(echo 'GET /'; echo ""; sleep 1; ) | telnet www.google.com 80

5
L'OP ha "* nix che non ha utilità da riga di comando per il download di file", quindi sicuramente non c'è lince.
Celada,

2
La nota lynx -sourceè più vicina a wget
Steven Penny,

Ehi, quindi questo è un commento davvero in ritardo ma come si salva l'output del comando telnet in un file? Il reindirizzamento con ">" genera sia il contenuto del file sia l'output telnet come "Prova 93.184.216.34 ... Connesso a www.example.com.". Sono in una situazione in cui posso usare solo telnet, sto provando a fare una prigione chroot con il minor numero di framework possibile.
pixelomer,

10

Adattato da Chris Snow answer Questo può anche gestire file di trasferimento binario

function __curl() {
  read proto server path <<<$(echo ${1//// })
  DOC=/${path// //}
  HOST=${server//:*}
  PORT=${server//*:}
  [[ x"${HOST}" == x"${PORT}" ]] && PORT=80

  exec 3<>/dev/tcp/${HOST}/$PORT
  echo -en "GET ${DOC} HTTP/1.0\r\nHost: ${HOST}\r\n\r\n" >&3
  (while read line; do
   [[ "$line" == $'\r' ]] && break
  done && cat) <&3
  exec 3>&-
}
  • rompo && cat per non leggere
  • utilizzo http 1.0 quindi non è necessario attendere / inviare una connessione: chiudi

Puoi testare file binari come questo

ivs@acsfrlt-j8shv32:/mnt/r $ __curl http://www.google.com/favicon.ico > mine.ico
ivs@acsfrlt-j8shv32:/mnt/r $ curl http://www.google.com/favicon.ico > theirs.ico
ivs@acsfrlt-j8shv32:/mnt/r $ md5sum mine.ico theirs.ico
f3418a443e7d841097c714d69ec4bcb8  mine.ico
f3418a443e7d841097c714d69ec4bcb8  theirs.ico

Questo non gestirà i file di trasferimento binario, non funzionerà su byte null.
Wildcard

@Wildcard, non capisco, ho modificato con un esempio di trasferimento file binario (contenente byte null), puoi indicarmi cosa mi sto perdendo?
131

2
@Wildcard, heheh, sì, sembra che dovrebbe funzionare, dal momento che legge i dati del file reale con cat. Non sono sicuro che sia un imbroglio (dal momento che non è puramente il guscio) o una buona soluzione (dal momento che catè uno strumento standard, dopo tutto). Ma @ 131, potresti voler aggiungere una nota sul perché funziona meglio delle altre soluzioni qui.
ilkkachu,

@Wildcard, ho aggiunto anche la soluzione pure bash come risposta di seguito. E sì, barare o no, questa è una soluzione valida e merita un voto :)
ilkkachu

7

Prendendo il " appena Bash e niente altro " strettamente, ecco un adattamento delle risposte precedenti ( @ Chris , @ 131 del ) che non chiamare qualsiasi utilità esterno (non anche quelli standard), ma funziona anche con i file binari:

#!/bin/bash
download() {
  read proto server path <<< "${1//"/"/ }"
  DOC=/${path// //}
  HOST=${server//:*}
  PORT=${server//*:}
  [[ x"${HOST}" == x"${PORT}" ]] && PORT=80

  exec 3<>/dev/tcp/${HOST}/$PORT

  # send request
  echo -en "GET ${DOC} HTTP/1.0\r\nHost: ${HOST}\r\n\r\n" >&3

  # read the header, it ends in a empty line (just CRLF)
  while IFS= read -r line ; do 
      [[ "$line" == $'\r' ]] && break
  done <&3

  # read the data
  nul='\0'
  while IFS= read -d '' -r x || { nul=""; [ -n "$x" ]; }; do 
      printf "%s$nul" "$x"
  done <&3
  exec 3>&-
}

Utilizzare con download http://path/to/file > file.

Abbiamo a che fare con byte NUL con read -d ''. Legge fino a un byte NUL e restituisce true se ne trova uno, false in caso contrario. Bash non è in grado di gestire i byte NUL nelle stringhe, quindi quando readritorna con true, aggiungiamo manualmente il byte NUL durante la stampa e quando restituisce false, sappiamo che non ci sono più byte NUL e questo dovrebbe essere l'ultimo pezzo di dati .

Testato con Bash 4.4 sui file con NUL nel mezzo, e termina a zero, uno o due NULs, e anche con l' wgete curlbinari da Debian. Il wgetdownload del file binario da 373 kB ha richiesto circa 5,7 secondi. Una velocità di circa 65 kB / se un po 'più di 512 kb / s.

In confronto, la soluzione cat di @ 131 termina in meno di 0,1 s, o quasi cento volte più velocemente. Non molto sorprendente, davvero.

Questo è ovviamente sciocco, poiché senza l'utilizzo di utility esterne, non c'è molto che possiamo fare con il file scaricato, nemmeno renderlo eseguibile.


L'eco non è un binario autonomo -non shell-? (: p)
131

1
@ 131, no! Bash ha echoe printfcome builtin (ha bisogno di un builtin printfper implementare printf -v)
ilkkachu

4

Se hai questo pacchetto libwww-perl

Puoi semplicemente usare:

/usr/bin/GET

Considerando che le altre risposte non rispettano il requisito della domanda (solo bash), penso che questo sia effettivamente migliore della lynxsoluzione, poiché Perl ha sicuramente più probabilità di essere preinstallato rispetto a Lynx.
Marcus

4

Usa invece il caricamento, tramite SSH dal tuo computer locale

Una casella "minimal headless * nix" significa probabilmente SSH in esso. Quindi puoi anche usare SSH per caricare su di esso. Che è funzionalmente equivalente al download (di pacchetti software ecc.) Tranne quando si desidera che un comando di download includa in uno script sul server senza testa, ovviamente.

Come mostrato in questa risposta , eseguiresti quanto segue sul tuo computer locale per posizionare un file sul tuo server headless remoto:

wget -O - http://example.com/file.zip | ssh user@host 'cat >/path/to/file.zip'

Caricamento più veloce tramite SSH da una terza macchina

Lo svantaggio della soluzione di cui sopra rispetto al download è una velocità di trasferimento inferiore, poiché la connessione con il computer locale ha generalmente una larghezza di banda molto inferiore rispetto alla connessione tra il server senza testa e altri server.

Per risolverlo, puoi ovviamente eseguire il comando sopra su un altro server con una larghezza di banda decente. Per renderlo più comodo (evitando un accesso manuale sul terzo computer), ecco un comando da eseguire sul tuo computer locale .

Per sicurezza, copia e incolla quel comando incluso il carattere spazio iniziale ' ' . Vedere le spiegazioni di seguito per il motivo.

 ssh user@intermediate-host "sshpass -f <(printf '%s\n' yourpassword) \
   ssh -T -e none \
     -o StrictHostKeyChecking=no \
     < <(wget -O - http://example.com/input-file.zip) \
     user@target-host \
     'cat >/path/to/output-file.zip' \
"

spiegazioni:

  • Il comando verrà inviato alla terza macchina intermediate-host, inizierà a scaricare un file lì wgete inizierà a caricarlo target-hosttramite SSH. Il download e il caricamento utilizzano la larghezza di banda del tuo intermediate-hoste avvengono contemporaneamente (a causa di equivalenti di Bash pipe), quindi i progressi saranno rapidi.

  • Quando si utilizza questo, è necessario sostituire i due accessi al server ( user@*-host), la password dell'host di destinazione ( yourpassword), l'URL di download ( http://example.com/…) e il percorso di output sull'host di destinazione ( /path/to/output-file.zip) con valori propri appropriati.

  • Per le -T -e noneopzioni SSH quando lo si utilizza per trasferire file, vedere queste spiegazioni dettagliate .

  • Questo comando è pensato per i casi in cui non è possibile utilizzare il meccanismo di autenticazione con chiave pubblica di SSH, ma continua con alcuni provider di hosting condiviso, in particolare Host Europe . Per automatizzare ancora il processo, ci affidiamo sshpassalla possibilità di fornire la password nel comando. Deve sshpassessere installato sul tuo host intermedio ( sudo apt-get install sshpasssotto Ubuntu).

  • Cerchiamo di utilizzare sshpassin modo sicuro, ma non sarà ancora sicuro come il meccanismo pubkey SSH (dice man sshpass). In particolare, forniamo la password SSH non come argomento della riga di comando ma tramite un file, che viene sostituito dalla sostituzione del processo bash per assicurarci che non esista mai sul disco. Il printfè un bash built-in, assicurandosi che questa parte del codice non pop-up come un comando separato in psuscita come che esporrebbe la password [ fonte ]. Io penso che questo uso di sshpassè altrettanto sicuro come il sshpass -d<file-descriptor>variante raccomandato man sshpass, perché bash mappe internamente a tale /dev/fd/*descrittore di file in ogni caso. E quello senza usare un file temporaneo [ fonte]. Ma nessuna garanzia, forse ho trascurato qualcosa.

  • Ancora una volta per rendere sshpasssicuro l' uso, dobbiamo impedire che il comando venga registrato nella cronologia bash sul tuo computer locale. Per questo, l'intero comando è preceduto da un carattere spazio, che ha questo effetto.

  • La -o StrictHostKeyChecking=noparte impedisce il fallimento del comando nel caso in cui non sia mai connessa all'host di destinazione. (Normalmente, SSH aspetterebbe quindi l'input dell'utente per confermare il tentativo di connessione. Facciamo comunque procedere.)

  • sshpasssi aspetta un comando ssho scpcome ultimo argomento. Quindi dobbiamo riscrivere il tipico wget -O - … | ssh …comando in un modulo senza una bash pipe, come spiegato qui .


3

Basato sulla ricetta @Chris Snow. Ho apportato alcuni miglioramenti:

  • controllo dello schema http (supporta solo http)
  • convalida della risposta http (controllo della riga dello stato della risposta e divisione dell'intestazione e del corpo per la riga '\ r \ n', non "Connessione: chiudi" che a volte non è vero)
  • non riuscito su codice non 200 (è importante scaricare file su Internet)

Ecco il codice:

function __wget() {
    : ${DEBUG:=0}
    local URL=$1
    local tag="Connection: close"

    if [ -z "${URL}" ]; then
        printf "Usage: %s \"URL\" [e.g.: %s http://www.google.com/]" \
               "${FUNCNAME[0]}" "${FUNCNAME[0]}"
        return 1;
    fi  
    read proto server path <<<$(echo ${URL//// })
    local SCHEME=${proto//:*}
    local PATH=/${path// //} 
    local HOST=${server//:*}
    local PORT=${server//*:}
    if [[ "$SCHEME" != "http" ]]; then
        printf "sorry, %s only support http\n" "${FUNCNAME[0]}"
        return 1
    fi  
    [[ x"${HOST}" == x"${PORT}" ]] && PORT=80
    [[ $DEBUG -eq 1 ]] && echo "SCHEME=$SCHEME" >&2
    [[ $DEBUG -eq 1 ]] && echo "HOST=$HOST" >&2
    [[ $DEBUG -eq 1 ]] && echo "PORT=$PORT" >&2
    [[ $DEBUG -eq 1 ]] && echo "PATH=$PATH" >&2

    exec 3<>/dev/tcp/${HOST}/$PORT
    if [ $? -ne 0 ]; then
        return $?
    fi  
    echo -en "GET ${PATH} HTTP/1.1\r\nHost: ${HOST}\r\n${tag}\r\n\r\n" >&3
    if [ $? -ne 0 ]; then
        return $?
    fi  
    # 0: at begin, before reading http response
    # 1: reading header
    # 2: reading body
    local state=0
    local num=0
    local code=0
    while read line; do
        num=$(($num + 1))
        # check http code
        if [ $state -eq 0 ]; then
            if [ $num -eq 1 ]; then
                if [[ $line =~ ^HTTP/1\.[01][[:space:]]([0-9]{3}).*$ ]]; then
                    code="${BASH_REMATCH[1]}"
                    if [[ "$code" != "200" ]]; then
                        printf "failed to wget '%s', code is not 200 (%s)\n" "$URL" "$code"
                        exec 3>&-
                        return 1
                    fi
                    state=1
                else
                    printf "invalid http response from '%s'" "$URL"
                    exec 3>&-
                    return 1
                fi
            fi
        elif [ $state -eq 1 ]; then
            if [[ "$line" == $'\r' ]]; then
                # found "\r\n"
                state=2
            fi
        elif [ $state -eq 2 ]; then
            # redirect body to stdout
            # TODO: any way to pipe data directly to stdout?
            echo "$line"
        fi
    done <&3
    exec 3>&-
}

Ottimi miglioramenti +1
Chris Snow,

Ha funzionato, ma ho trovato una preoccupazione, quando uso questi script, continua ad attendere diversi secondi quando tutti i dati vengono letti, questo caso non si verifica nella risposta di @Chris Snow, qualcuno potrebbe spiegarlo?
zw963,

E, in questa risposta, echo -en "GET ${PATH} HTTP/1.1\r\nHost: ${HOST}\r\n${tag}\r\n\r\n" >&3, ${tag}non è specificato.
zw963,

Modifico questa risposta con la tagvariabile impostata correttamente, ora funziona bene.
zw963,

non funziona con zsh, __wget google.com mi dispiace, supporta solo http / usr / bin / env: bash: nessun file o directory del genere
vrkansagara
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.