cara mengunduh file menggunakan bash saja dan tidak ada yang lain (tidak ada curl, wget, perl, dll.)

40

Saya memiliki minimal * nix tanpa kepala yang tidak memiliki utilitas baris perintah untuk mengunduh file (mis. Tidak ada ikal, wget, dll). Saya hanya punya bash.

Bagaimana saya bisa mengunduh file?

Idealnya, saya ingin solusi yang akan bekerja di berbagai * nix.

Chris Snow
sumber
bagaimana dengangawk
Neil McGuigan
Saya tidak ingat sekarang jika gawk tersedia, walaupun saya ingin melihat solusi berbasis gawk jika Anda memilikinya :)
Chris Snow
1
inilah sebuah contoh: gnu.org/software/gawk/manual/gawkinet/gawkinet.html#Web-page
Neil McGuigan

Jawaban:

64

Jika Anda memiliki bash 2.04 atau lebih baru dengan perangkat /dev/tcppseudo diaktifkan, Anda dapat mengunduh file dari bash itu sendiri.

Rekatkan kode berikut langsung ke bash shell (Anda tidak perlu menyimpan kode ke file untuk dieksekusi):

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

Kemudian Anda bisa menjalankannya dari shell sebagai berikut:

__wget http://example.iana.org/

Sumber: Moreaki 's jawaban upgrade dan menginstal paket melalui baris perintah cygwin?

Pembaruan: sebagaimana disebutkan dalam komentar, pendekatan yang diuraikan di atas adalah sederhana:

  • surat readwasiat tersebut akan menghapus backslash dan memimpin spasi putih.
  • Bash tidak bisa menangani byte NUL dengan sangat baik sehingga file biner keluar.
  • tekad bulat $linekutip.
Chris Snow
sumber
8
Jadi Anda menjawab pertanyaan Anda sendiri pada saat yang sama saat Anda menanyakannya. Itu mesin waktu yang menarik yang Anda miliki;)
Meer Borg
11
@MeerBorg - ketika Anda mengajukan pertanyaan, cari kotak centang 'jawab pertanyaan Anda sendiri' - blog.stackoverflow.com/2011/07/...
Chris Snow
@ eestartup - Saya rasa Anda tidak bisa memberikan suara untuk jawaban Anda sendiri. Bisakah saya menjelaskan kodenya? Belum! Tapi itu berhasil pada cygwin.
Chris Snow
3
Hanya sebuah catatan: Ini tidak akan berfungsi dengan beberapa konfigurasi Bash. Saya percaya Debian mengkonfigurasi fitur ini dari distribusi Bash mereka.
1
Urgh, walaupun ini adalah trik yang bagus, itu bisa dengan mudah menyebabkan unduhan yang rusak. while readseperti itu sampah backslash dan spasi putih terkemuka dan Bash tidak bisa menangani byte NUL dengan sangat baik sehingga file biner keluar. Dan tanda kutip $lineakan glob ... Tak satu pun dari ini saya lihat disebutkan dalam jawaban.
ilkkachu
19

Gunakan lynx.

Ini cukup umum untuk sebagian besar Unix / Linux.

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

-dump: membuang file pertama ke stdout dan keluar

man lynx

Atau netcat:

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

Atau telnet:

(echo 'GET /'; echo ""; sleep 1; ) | telnet www.google.com 80
tumpukan kayu
sumber
5
OP memiliki "* nix yang tidak memiliki utilitas baris perintah untuk mengunduh file", jadi tidak ada lynx yang pasti.
Celada
2
Catatan lynx -sourcelebih dekat dengan wget
Steven Penny
Hai, jadi ini adalah komentar yang sangat terlambat, tetapi bagaimana Anda menyimpan output dari perintah telnet ke file? Mengarahkan kembali dengan ">" menampilkan konten file dan output telnet seperti "Mencoba 93.184.216.34 ... Terhubung ke www.example.com.". Saya berada dalam situasi di mana saya hanya bisa menggunakan telnet, saya mencoba membuat chroot jail dengan kerangka kerja sekecil mungkin.
pixelomer
10

Diadaptasi dari jawaban Chris Snow Ini juga dapat menangani file transfer biner

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>&-
}
  • Saya menghancurkan && kucing agar tidak terbaca
  • saya menggunakan http 1.0 sehingga tidak perlu menunggu / mengirim koneksi: tutup

Anda dapat menguji file biner seperti ini

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
131
sumber
Ini tidak akan menangani file transfer biner — itu akan gagal pada byte nol.
Wildcard
@ Kartu Memori, saya tidak mengerti, saya telah mengedit dengan contoh transfer file biner (berisi byte nol), dapatkah Anda menunjukkan kepada saya apa yang saya lewatkan?
131
2
@ Kartu Wild, heheh, ya itu sepertinya itu harus berfungsi, karena membaca data file aktual dengan cat. Saya tidak yakin apakah itu curang (karena itu bukan murni shell), atau solusi yang bagus (karena catmerupakan alat standar, setelah semua). Tapi @ 131, Anda mungkin ingin menambahkan catatan tentang mengapa ini bekerja lebih baik daripada solusi lain di sini.
ilkkachu
@ Kartu Memori, saya menambahkan solusi bash murni juga sebagai jawaban di bawah ini. Dan ya, curang atau tidak, ini adalah solusi yang valid dan layak mendapat upvote :)
ilkkachu
7

Mengambil " Bash saja dan tidak ada yang lain " dengan ketat, inilah satu adaptasi dari jawaban sebelumnya ( @ Chris's , @ 131's ) yang tidak memanggil utilitas eksternal apa pun (bahkan yang standar) tetapi juga berfungsi dengan file biner:

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

Gunakan dengan download http://path/to/file > file.

Kami menangani byte NUL dengan read -d ''. Bunyinya sampai byte NUL, dan mengembalikan true jika menemukan satu, false jika tidak. Bash tidak dapat menangani byte NUL dalam string, jadi ketika readkembali dengan true, kami menambahkan byte NUL secara manual saat mencetak, dan ketika itu mengembalikan false, kita tahu tidak ada byte NUL lagi, dan ini harus menjadi bagian terakhir dari data .

Diuji dengan Bash 4.4 pada file dengan NUL di tengah, dan berakhir di nol, satu atau dua NUL, dan juga dengan wgetdan curlbinari dari Debian. wgetBiner 373 kB memerlukan waktu sekitar 5,7 detik untuk diunduh. Kecepatan sekitar 65 kB / dtk atau sedikit lebih dari 512 kb / dtk.

Sebagai perbandingan, solusi kucing @ 131 selesai dalam waktu kurang dari 0,1 detik, atau hampir seratus kali lebih cepat. Tidak terlalu mengejutkan, sungguh.

Ini jelas konyol, karena tanpa menggunakan utilitas eksternal, tidak banyak yang bisa kita lakukan dengan file yang diunduh, bahkan membuatnya tidak dapat dieksekusi.

ilkkachu
sumber
Bukankah menggemakan biner -non shell-mandiri? (: p)
131
1
@ 131, tidak! Bash memiliki echodan printfsebagai builtin (perlu builtin printfuntuk mengimplementasikannya printf -v)
ilkkachu
4

Jika Anda memiliki paket ini libwww-perl

Anda cukup menggunakan:

/usr/bin/GET
stackexchanger
sumber
Mempertimbangkan bahwa jawaban lain tidak menghormati persyaratan pertanyaan (hanya bash), saya pikir ini sebenarnya lebih baik daripada lynxsolusinya, karena Perl pasti lebih cenderung diinstal sebelumnya daripada Lynx.
Marcus
4

Gunakan unggah sebagai gantinya, melalui SSH dari mesin lokal Anda

Kotak "minimal tanpa kepala * nix" berarti Anda mungkin memasukkan SSH ke dalamnya. Jadi, Anda juga dapat menggunakan SSH untuk mengunggahnya . Yang secara fungsional setara dengan pengunduhan (paket perangkat lunak, dll.) Kecuali bila Anda ingin perintah pengunduhan disertakan dalam skrip pada server tanpa kepala Anda, tentu saja.

Seperti yang ditunjukkan dalam jawaban ini , Anda akan menjalankan yang berikut ini di mesin lokal Anda untuk menempatkan file di server tanpa kepala jarak jauh Anda:

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

Mengunggah lebih cepat melalui SSH dari mesin ketiga

Kerugian dari solusi di atas dibandingkan dengan mengunduh adalah kecepatan transfer yang lebih rendah, karena koneksi dengan mesin lokal Anda biasanya memiliki bandwidth yang jauh lebih sedikit daripada koneksi antara server tanpa kepala Anda dan server lain.

Untuk mengatasi itu, Anda tentu saja dapat menjalankan perintah di atas pada server lain dengan bandwidth yang layak. Untuk membuatnya lebih nyaman (menghindari login manual pada mesin ketiga), berikut adalah perintah untuk dijalankan pada mesin lokal Anda .

Agar aman, salin & tempel perintah itu termasuk karakter spasi terkemuka ' ' . Lihat penjelasan di bawah ini untuk alasannya.

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

Penjelasan:

  • Perintah akan ssh ke mesin ketiga Anda intermediate-host, mulai mengunduh file ke sana melalui wget, dan mulai mengunggahnya ke target-hostmelalui SSH. Mengunduh dan mengunggah menggunakan bandwidth Anda intermediate-hostdan terjadi pada saat yang sama (karena setara dengan Bash pipe), sehingga kemajuan akan cepat.

  • Saat menggunakan ini, Anda harus mengganti dua login server ( user@*-host), kata sandi host target ( yourpassword), URL unduhan ( http://example.com/…) dan jalur output pada host target Anda ( /path/to/output-file.zip) dengan nilai sendiri yang sesuai.

  • Untuk -T -e noneopsi SSH saat menggunakannya untuk mentransfer file, lihat penjelasan terperinci ini .

  • Perintah ini dimaksudkan untuk kasus di mana Anda tidak dapat menggunakan mekanisme otentikasi kunci publik SSH - itu masih terjadi dengan beberapa penyedia hosting bersama, terutama Host Eropa . Untuk tetap mengotomatiskan proses, kami bergantung sshpassuntuk dapat memasok kata sandi dalam perintah. Itu perlu sshpassdiinstal pada host perantara Anda (di sudo apt-get install sshpassbawah Ubuntu).

  • Kami mencoba menggunakan sshpassdengan cara yang aman, tetapi masih tidak seaman mekanisme pubkey SSH (kata man sshpass). Secara khusus, kami menyediakan kata sandi SSH bukan sebagai argumen baris perintah tetapi melalui file, yang digantikan oleh substitusi proses bash untuk memastikannya tidak pernah ada pada disk. Ini printfadalah built-in bash, memastikan bagian kode ini tidak muncul sebagai perintah terpisah dalam psoutput karena itu akan mengekspos kata sandi [ sumber ]. Saya pikir penggunaan sshpassini sama amannya dengan sshpass -d<file-descriptor>varian yang direkomendasikan man sshpass, karena bash memetakannya secara internal ke /dev/fd/*deskriptor file semacam itu. Dan itu tanpa menggunakan file temp [ sumber] Tapi tidak ada jaminan, mungkin saya mengabaikan sesuatu.

  • Sekali lagi untuk membuat sshpasspenggunaan aman, kita perlu mencegah perintah agar tidak direkam ke bash history di mesin lokal Anda. Untuk itu, seluruh perintah diawali dengan satu karakter spasi, yang memiliki efek ini.

  • Bagian -o StrictHostKeyChecking=nomencegah perintah gagal jika itu tidak pernah terhubung ke host target. (Biasanya, SSH kemudian akan menunggu input pengguna untuk mengonfirmasi upaya koneksi. Kami tetap melanjutkannya.)

  • sshpassmengharapkan a sshatau scpperintah sebagai argumen terakhirnya. Jadi kita harus menulis ulang wget -O - … | ssh …perintah khas menjadi bentuk tanpa bash pipe, seperti yang dijelaskan di sini .

tanius
sumber
3

Berdasarkan resep @Chris Snow. Saya membuat beberapa peningkatan:

  • Periksa skema http (hanya mendukung http)
  • Validasi respons http (pemeriksaan baris status respons, dan pisahkan tajuk dan badan dengan garis '\ r \ n', bukan 'Sambungan: tutup' yang kadang-kadang tidak benar)
  • gagal pada kode non-200 (penting untuk mengunduh file di internet)

Ini kode:

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>&-
}
Yecheng Fu
sumber
Peningkatan yang bagus +1
Chris Snow
Berhasil, tetapi saya menemukan kekhawatiran, ketika saya menggunakan skrip ini, tetap menunggu beberapa detik ketika semua data selesai dibaca, kasus ini tidak terjadi di @ Chris Snow jawaban, ada yang bisa menjelaskan ini?
zw963
Dan, dalam jawaban ini echo -en "GET ${PATH} HTTP/1.1\r\nHost: ${HOST}\r\n${tag}\r\n\r\n" >&3,, ${tag}tidak ditentukan.
zw963
Saya mengedit jawaban ini dengan tagvariabel yang disetel dengan benar, sekarang berfungsi dengan baik.
zw963
tidak bekerja dengan zsh, __wget google.com maaf, hanya mendukung http / usr / bin / env: bash: Tidak ada file atau direktori seperti itu
vrkansagara