làm cách nào để tải xuống một tệp chỉ bằng bash và không có gì khác (không có curl, wget, perl, v.v.)


40

Tôi có một tiêu đề tối thiểu * nix không có bất kỳ tiện ích dòng lệnh nào để tải xuống các tệp (ví dụ: không curl, wget, v.v.). Tôi chỉ có bash.

Làm thế nào tôi có thể tải xuống một tập tin?

Lý tưởng nhất, tôi muốn một giải pháp sẽ hoạt động trên phạm vi rộng * nix.


làm thế nào vềgawk
Neil McGuigan

Bây giờ tôi không thể nhớ nếu gawk có sẵn, mặc dù tôi rất thích xem giải pháp dựa trên gawk nếu bạn có :)
Chris Snow

Câu trả lời:


64

Nếu bạn có bash 2.04 trở lên với /dev/tcpthiết bị giả được kích hoạt, bạn có thể tải xuống một tệp từ chính bash.

Dán mã sau đây trực tiếp vào bash shell (bạn không cần lưu mã vào tệp để thực thi):

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

Sau đó, bạn có thể thực hiện nó như từ shell như sau:

__wget http://example.iana.org/

Nguồn: Câu trả lời của Moreaki nâng cấp và cài đặt các gói thông qua dòng lệnh cygwin?

Cập nhật: như đã đề cập trong bình luận, cách tiếp cận được nêu ở trên rất đơn giản:

  • ý readchí sẽ xóa dấu gạch chéo ngược và khoảng trắng hàng đầu.
  • Bash không thể xử lý các byte NUL rất độc đáo để các tệp nhị phân bị loại ra.
  • không được trích dẫn $linesẽ toàn cầu.

8
Vì vậy, bạn đã trả lời câu hỏi của riêng bạn cùng lúc với bạn hỏi nó. Đó là một cỗ máy thời gian thú vị mà bạn có;)
Meer Borg

11
@MeerBorg - khi bạn đặt câu hỏi, hãy tìm hộp đánh dấu 'trả lời câu hỏi của chính bạn' - blog.stackoverflow.com/2011/07/ trên
Chris Snow

@eestartup - Tôi không nghĩ bạn có thể bỏ phiếu cho câu trả lời của riêng bạn. Tôi có thể giải thích mã? Chưa! Nhưng nó hoạt động trên cygwin.
Chris Snow

3
Chỉ cần lưu ý: Điều này sẽ không hoạt động với một số cấu hình của Bash. Tôi tin rằng Debian cấu hình tính năng này từ bản phân phối Bash của họ.

1
Urgh, trong khi đây là một mẹo hay, nó có thể dễ dàng gây ra các tải xuống bị hỏng. while readnhư thế sẽ xóa các dấu gạch chéo ngược và khoảng trắng hàng đầu và Bash không thể xử lý các byte NUL rất độc đáo để các tệp nhị phân được đưa ra ngoài. Và không được trích dẫn $linesẽ toàn cầu ... Không ai trong số này tôi thấy được đề cập trong câu trả lời.
ilkkachu

19

Sử dụng lynx.

Nó là khá phổ biến đối với hầu hết Unix / Linux.

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

-dump: kết xuất tập tin đầu tiên vào thiết bị xuất chuẩn và thoát

man lynx

Hoặc mạng mèo:

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

Hoặc telnet:

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

5
OP có "* nix không có bất kỳ tiện ích dòng lệnh nào để tải xuống tệp", do đó không có lynx nào chắc chắn.
Celada

2
Lưu ý lynx -sourcelà gần hơn với wget
Steven Penny

Này, vậy đây là một nhận xét thực sự muộn nhưng làm thế nào để bạn lưu đầu ra của lệnh telnet vào một tệp? Chuyển hướng với ">" xuất ra cả nội dung của tệp và đầu ra telnet, chẳng hạn như "Đang thử 93.184.216.34 ... Đã kết nối với www.example.com.". Tôi đang ở trong một tình huống mà tôi chỉ có thể sử dụng telnet, tôi đang cố gắng tạo ra một nhà tù chroot với ít khung nhất có thể.
pixelome

10

Chuyển thể từ câu trả lời của Chris Snow Điều này cũng có thể xử lý các tệp chuyển nhị phân

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>&-
}
  • tôi phá vỡ && mèo để ra khỏi đọc
  • tôi sử dụng http 1.0 vì vậy không cần phải đợi / gửi kết nối: đóng

Bạn có thể kiểm tra các tệp nhị phân như thế này

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

Điều này sẽ không xử lý các tập tin chuyển nhị phân. Nó sẽ thất bại trên các byte rỗng.
tự đại diện

@Wildcard, tôi không hiểu, tôi đã chỉnh sửa bằng ví dụ truyền tệp nhị phân (chứa byte rỗng), bạn có thể chỉ cho tôi những gì tôi đang thiếu không?
131

2
@Wildcard, heheh, yeah có vẻ như nó nên hoạt động, vì nó đọc dữ liệu tệp thực tế với cat. Tôi không chắc đó là gian lận (vì nó không hoàn toàn là vỏ), hay một giải pháp tốt đẹp (vì rốt cuộc đó catlà một công cụ tiêu chuẩn). Nhưng @ 131, bạn có thể muốn thêm một lưu ý về lý do tại sao nó hoạt động tốt hơn các giải pháp khác ở đây.
ilkkachu

@Wildcard, tôi đã thêm giải pháp bash thuần túy như một câu trả lời dưới đây. Và có, gian lận hay không, đây là một giải pháp hợp lệ và đáng để nâng cao :)
ilkkachu

7

Thực hiện nghiêm túc " chỉ Bash và không có gì khác ", đây là một bản phóng tác của các câu trả lời trước đó ( @ Chris , @ 131 ) không gọi bất kỳ tiện ích bên ngoài nào (thậm chí không phải là tiêu chuẩn) mà còn hoạt động với các tệp nhị phân:

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

Sử dụng với download http://path/to/file > file.

Chúng tôi xử lý các byte NUL với read -d ''. Nó đọc cho đến một byte NUL và trả về true nếu tìm thấy một byte, sai nếu không. Bash không thể xử lý các byte NUL trong chuỗi, vì vậy khi readtrả về bằng true, chúng ta thêm byte NUL theo cách thủ công khi in và khi nó trả về false, chúng ta biết không còn byte NUL nào nữa và đây sẽ là phần dữ liệu cuối cùng .

Đã thử nghiệm với Bash 4.4 trên các tệp có NUL ở giữa và kết thúc bằng 0, một hoặc hai NUL và cả với các nhị phân wgetcurlDebian từ Debian. wgetNhị phân 373 kB mất khoảng 5,7 giây để tải xuống. Tốc độ khoảng 65 kB / s hoặc hơn một chút so với 512 kb / s.

So sánh, dung dịch mèo của @ 131 kết thúc sau chưa đầy 0,1 giây, hoặc nhanh hơn gần một trăm lần. Không có gì đáng ngạc nhiên, thực sự.

Điều này rõ ràng là ngớ ngẩn, vì không sử dụng các tiện ích bên ngoài, chúng ta không thể làm gì nhiều với tệp đã tải xuống, thậm chí không thể thực hiện được.


Không lặp lại một shell -non độc lập - nhị phân? (: p)
131

1
@ 131, không! Bash có echoprintfnhư các nội dung (nó cần một nội dung printfđể triển khai printf -v)
ilkkachu

4

Nếu bạn có gói này libwww-perl

Bạn chỉ có thể sử dụng:

/usr/bin/GET

Xem xét rằng các câu trả lời khác không tôn trọng yêu cầu câu hỏi (chỉ bash), tôi nghĩ rằng điều này thực sự tốt hơn lynxgiải pháp, vì Perl chắc chắn có nhiều khả năng được cài đặt sẵn Lynx.
Marcus

4

Thay vào đó, hãy sử dụng tải lên, thông qua SSH từ máy cục bộ của bạn

Hộp "tối thiểu không đầu * nix" có nghĩa là bạn có thể SSH vào nó. Vì vậy, bạn cũng có thể sử dụng SSH để tải lên nó. Chức năng tương đương với tải xuống (của các gói phần mềm, v.v.) trừ khi bạn muốn có lệnh tải xuống để đưa vào tập lệnh trên máy chủ không đầu của bạn.

Như được hiển thị trong câu trả lời này , bạn sẽ thực hiện các thao tác sau trên máy cục bộ của mình để đặt tệp trên máy chủ không đầu từ xa của bạn:

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

Tải lên nhanh hơn qua SSH từ máy thứ ba

Nhược điểm của giải pháp trên so với tải xuống là tốc độ truyền thấp hơn, vì kết nối với máy cục bộ của bạn thường có băng thông ít hơn nhiều so với kết nối giữa máy chủ không đầu của bạn và các máy chủ khác.

Để giải quyết điều đó, tất nhiên bạn có thể thực hiện lệnh trên trên một máy chủ khác với băng thông khá. Để làm cho điều đó thoải mái hơn (tránh đăng nhập thủ công trên máy thứ ba), đây là lệnh thực thi trên máy cục bộ của bạn .

Để bảo mật, sao chép và dán lệnh đó bao gồm ký tự khoảng trắng hàng đầu ' ' . Xem giải thích dưới đây cho lý do.

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

Giải thích:

  • Lệnh sẽ ssh đến máy thứ ba của bạn intermediate-host, bắt đầu tải tệp xuống đó thông qua wgetvà bắt đầu tải nó lên target-hostqua SSH. Tải xuống và tải lên sử dụng băng thông của bạn intermediate-hostvà xảy ra cùng một lúc (do tương đương với ống Bash), vì vậy tiến trình sẽ nhanh chóng.

  • Khi sử dụng, bạn phải thay thế hai thông tin đăng nhập máy chủ ( user@*-host), mật khẩu máy chủ đích ( yourpassword), URL tải xuống ( http://example.com/…) và đường dẫn đầu ra trên máy chủ đích của bạn ( /path/to/output-file.zip) bằng các giá trị riêng phù hợp.

  • Đối với các -T -e nonetùy chọn SSH khi sử dụng nó để truyền tệp, hãy xem các giải thích chi tiết này .

  • Lệnh này dành cho các trường hợp bạn không thể sử dụng cơ chế xác thực khóa công khai của SSH - nó vẫn xảy ra với một số nhà cung cấp dịch vụ lưu trữ được chia sẻ, đặc biệt là Host Europe . Để vẫn tự động hóa quy trình, chúng tôi dựa vào sshpassđể có thể cung cấp mật khẩu trong lệnh. Nó đòi hỏi sshpassphải được cài đặt trên máy chủ trung gian của bạn ( sudo apt-get install sshpasstrong Ubuntu).

  • Chúng tôi cố gắng sử dụng sshpassmột cách an toàn, nhưng nó vẫn sẽ không an toàn như cơ chế pubkey SSH (nói man sshpass). Cụ thể, chúng tôi cung cấp mật khẩu SSH không phải là một đối số dòng lệnh mà thông qua một tệp, được thay thế bằng thay thế quá trình bash để đảm bảo nó không bao giờ tồn tại trên đĩa. Đây printflà một bash tích hợp, đảm bảo phần mã này không bật lên dưới dạng một lệnh riêng biệt trong psđầu ra vì điều đó sẽ làm lộ mật khẩu [ nguồn ]. Tôi nghĩ rằng việc sử dụng sshpassnày cũng an toàn như sshpass -d<file-descriptor>biến thể được đề xuất man sshpass, bởi vì bash ánh xạ bên trong nó đến một /dev/fd/*mô tả tệp như vậy . Và không cần sử dụng tệp tạm thời [ nguồn]. Nhưng không có gì đảm bảo, có lẽ tôi đã bỏ qua một cái gì đó.

  • Một lần nữa để làm cho việc sshpasssử dụng an toàn, chúng tôi cần ngăn lệnh được ghi vào lịch sử bash trên máy cục bộ của bạn. Do đó, toàn bộ lệnh được thêm vào một ký tự khoảng trắng, có hiệu ứng này.

  • Phần này -o StrictHostKeyChecking=nongăn không cho lệnh bị lỗi trong trường hợp nó không bao giờ được kết nối với máy chủ đích. (Thông thường, SSH sau đó sẽ đợi đầu vào của người dùng để xác nhận nỗ lực kết nối. Chúng tôi vẫn tiếp tục.)

  • sshpassmong đợi một sshhoặc scplệnh như là đối số cuối cùng của nó. Vì vậy, chúng ta phải viết lại wget -O - … | ssh …lệnh điển hình thành một biểu mẫu mà không có ống bash, như được giải thích ở đây .


3

Dựa trên công thức @Chris Snow. Tôi đã thực hiện một số cải tiến:

  • kiểm tra sơ đồ http (nó chỉ hỗ trợ http)
  • Xác thực phản hồi http (kiểm tra dòng trạng thái phản hồi và phân tách tiêu đề và nội dung theo dòng '\ r \ n', không phải 'Kết nối: đóng' đôi khi không đúng)
  • không thành công với mã không phải 200 (điều quan trọng là tải xuống các tệp trên internet)

Đây là mã:

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

Những cải tiến tuyệt vời +1
Chris Snow

Nó đã hoạt động, nhưng tôi thấy một mối lo ngại, khi tôi sử dụng tập lệnh này, Nó cứ đợi vài giây khi tất cả dữ liệu được đọc xong, trường hợp này không xảy ra trong câu trả lời @Chris Snow, có ai có thể giải thích điều này không?
zw963

Và, trong câu trả lời này echo -en "GET ${PATH} HTTP/1.1\r\nHost: ${HOST}\r\n${tag}\r\n\r\n" >&3, ${tag}không được chỉ định.
zw963

Tôi chỉnh sửa câu trả lời này với tagbiến được đặt chính xác, bây giờ nó hoạt động tốt.
zw963

không hoạt động với zsh, __wget google.com xin lỗi, chỉ hỗ trợ http / usr / bin / env: bash: Không có tệp hoặc thư mục như vậy
vrkansagara 14/12/17
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.