web-dev-qa-db-fra.com

comment télécharger un fichier en utilisant simplement bash et rien d'autre (pas de curl, wget, Perl, etc.)

J'ai un minimum nix * sans tête qui n'a pas tous les utilitaires de ligne de commande pour télécharger des fichiers (par exemple pas de boucle, wget, etc.). Je n'ai que bash.

Comment télécharger un fichier?

Idéalement, je voudrais une solution qui fonctionnerait sur une large gamme de * nix.

45
Chris Snow

Si vous avez bash 2.04 ou supérieur avec le /dev/tcp pseudo-périphérique activé, vous pouvez télécharger un fichier depuis bash lui-même.

Collez le code suivant directement dans un shell bash (vous n'avez pas besoin d'enregistrer le code dans un fichier pour l'exécution):

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>&-
}

Ensuite, vous pouvez l'exécuter à partir du shell comme suit:

__wget http://example.iana.org/

Source: Moreaki 's answer mise à niveau et installation de packages via la ligne de commande cygwin?

Mise à jour: comme mentionné dans le commentaire, l'approche décrite ci-dessus est simpliste:

  • le read supprimera les barres obliques inverses et les espaces blancs de début.
  • Bash ne peut pas très bien gérer les octets NUL, donc les fichiers binaires sont sortis.
  • non cité $line sera global.
67
Chris Snow

Utilisez le lynx.

C'est assez courant pour la plupart d'Unix/Linux.

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

-dump: vide le premier fichier sur stdout et quitte

man lynx

Ou netcat:

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

Ou telnet:

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

Adapté de la réponse de Chris Snow Cela peut également gérer les fichiers de transfert binaires

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>&-
}
  • je casse & & cat pour sortir de la lecture
  • j'utilise http 1.0 donc pas besoin d'attendre/envoyer une connexion: fermer

Vous pouvez tester des fichiers binaires comme celui-ci

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
11
131

En prenant strictement " juste Bash et rien d'autre", voici une adaptation des réponses précédentes ( @ Chris's , @ 131's ) qui n'appelle aucun utilitaire externe (même pas standard) mais fonctionne également avec des fichiers binaires:

#!/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>&-
}

Utiliser avec download http://path/to/file > file.

Nous traitons les octets NUL avec read -d ''. Il lit jusqu'à un octet NUL et renvoie vrai s'il en a trouvé un, faux s'il ne l'a pas fait. Bash ne peut pas gérer les octets NUL dans les chaînes, donc quand read retourne avec true, nous ajoutons l'octet NUL manuellement lors de l'impression, et quand il retourne false, nous savons qu'il n'y a plus d'octets NUL, et cela devrait être la dernière donnée.

Testé avec Bash 4.4 sur des fichiers avec NUL au milieu, et se terminant par zéro, un ou deux NUL, ainsi qu'avec les binaires wget et curl de Debian. Le téléchargement du binaire _ 373 kB wget a pris environ 5,7 secondes. Une vitesse d'environ 65 kB/s ou un peu plus de 512 kb/s.

En comparaison, la solution de chat de @ 131 se termine en moins de 0,1 s, soit presque cent fois plus vite. Pas très surprenant, vraiment.

C'est évidemment idiot, car sans utiliser des utilitaires externes, nous ne pouvons pas faire grand-chose avec le fichier téléchargé, ni même le rendre exécutable.

8
ilkkachu

Utilisez plutôt le téléchargement via SSH depuis votre machine locale

Une boîte "minimal headless * nix" signifie que vous y connectez probablement SSH. Vous pouvez donc également utiliser SSH pour télécharger. Ce qui est fonctionnellement équivalent au téléchargement (de progiciels, etc.) sauf lorsque vous voulez une commande de téléchargement à inclure dans un script sur votre serveur sans tête bien sûr.

Comme indiqué dans cette réponse , vous exécuteriez ce qui suit sur votre machine locale pour placer un fichier sur votre serveur sans tête distant:

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

Téléchargement plus rapide via SSH depuis une troisième machine

L'inconvénient de la solution ci-dessus par rapport au téléchargement est une vitesse de transfert inférieure, car la connexion avec votre machine locale a généralement beaucoup moins de bande passante que la connexion entre votre serveur sans tête et d'autres serveurs.

Pour résoudre cela, vous pouvez bien sûr exécuter la commande ci-dessus sur un autre serveur avec une bande passante décente. Pour rendre cela plus confortable (en évitant une connexion manuelle sur la troisième machine), voici une commande pour exécuter sur votre machine locale.

Pour être sûr, copiez et collez cette commande y compris le premier caractère d'espace' '. Voir les explications ci-dessous pour la raison.

 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' \
"

Explications:

  • La commande ssh vers votre troisième machine intermediate-Host, Commencera à y télécharger un fichier via wget, et commencera à le télécharger vers target-Host Via SSH. Le téléchargement et le téléchargement utilisent la bande passante de votre intermediate-Host Et se produisent en même temps (en raison des équivalents de Bash Pipe), donc les progrès seront rapides.

  • Lorsque vous utilisez ceci, vous devez remplacer les deux connexions serveur (user@*-Host), Le mot de passe hôte cible (yourpassword), l'URL de téléchargement (http://example.com/…) Et le chemin de sortie sur votre hôte cible (/path/to/output-file.Zip) avec ses propres valeurs appropriées.

  • Pour les options SSH -T -e none Lors de son utilisation pour transférer des fichiers, voir ces explications détaillées .

  • Cette commande est destinée aux cas où vous ne pouvez pas utiliser le mécanisme d'authentification par clé publique de SSH - cela se produit toujours avec certains fournisseurs d'hébergement partagé, notamment Host Europe . Pour automatiser encore le processus, nous comptons sur sshpass pour pouvoir fournir le mot de passe dans la commande. Il nécessite que sshpass soit installé sur votre hôte intermédiaire (Sudo apt-get install sshpass Sous Ubuntu).

  • Nous essayons d'utiliser sshpass de manière sécurisée, mais ce ne sera toujours pas aussi sécurisé que le mécanisme SSH pubkey (dit man sshpass). En particulier, nous fournissons le mot de passe SSH non pas comme un argument de ligne de commande mais via un fichier, qui est remplacé par la substitution de processus bash pour nous assurer qu'il n'existe jamais sur le disque. printf est un bash intégré, s'assurant que cette partie du code n'apparaît pas comme une commande distincte dans la sortie ps car cela exposerait le mot de passe [ source ]. I pensez que cette utilisation de sshpass est aussi sécurisée que la variante sshpass -d<file-descriptor> Recommandée dans man sshpass, Parce que bash la mappe en interne à une telle un descripteur de fichier /dev/fd/* de toute façon. Et cela sans utiliser de fichier temporaire [ source ]. Mais aucune garantie, j'ai peut-être oublié quelque chose.

  • Encore une fois pour sécuriser l'utilisation de sshpass, nous devons empêcher l'enregistrement de la commande dans l'historique bash de votre machine locale. Pour cela, la commande entière est précédée d'un caractère espace, ce qui a cet effet.

  • La partie -o StrictHostKeyChecking=no Empêche la commande d'échouer au cas où elle ne se serait jamais connectée à l'hôte cible. (Normalement, SSH attendrait alors l'entrée de l'utilisateur pour confirmer la tentative de connexion. Nous le faisons quand même.)

  • sshpass attend une commande ssh ou scp comme dernier argument. Nous devons donc réécrire la commande wget -O - … | ssh … Typique dans un formulaire sans pipe bash, comme expliqué ici .

4
tanius

Si vous avez ce paquet libwww-Perl

Vous pouvez simplement utiliser:

/usr/bin/GET
4
stackexchanger

Basé sur la recette @Chris Snow. J'ai fait quelques améliorations:

  • vérification du schéma http (il ne prend en charge que http)
  • validation de la réponse http (vérification de la ligne d'état de la réponse et fractionnement de l'en-tête et du corps par la ligne '\ r\n', pas 'Connection: close', ce qui n'est pas vrai parfois)
  • a échoué sur un code non-200 (il est important de télécharger des fichiers sur Internet)

Voici le code:

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>&-
}
3
Yecheng Fu