如何仅使用bash而不是其他文件(没有curl,wget,perl等)下载文件


40

我有一个最小的无头* nix,它没有任何命令行实用程序来下载文件(例如,curl,wget等)。我只有bash。

如何下载文件?

理想情况下,我想要一个适用于广泛* nix的解决方案。


怎么样gawk
Neil McGuigan

我现在不记得gawk是否可用,尽管如果有的话,我很想看看基于gawk的解决方案:)
克里斯·斯诺

Answers:


64

如果您具有/dev/tcp启用了伪设备的bash 2.04或更高版本,则可以从bash本身下载文件。

将以下代码直接粘贴到bash shell中(您无需将代码保存到文件中即可执行):

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

然后,您可以从外壳程序执行它,如下所示:

__wget http://example.iana.org/

资料来源:Moreaki 通过cygwin命令行升级和安装软件包的答案

更新: 如评论中所述,上面概述的方法很简单:

  • read意志象垃圾一样清除反斜线和前导空格。
  • Bash不能很好地处理NUL字节,因此二进制文件不可用。
  • 未引用的$line会引起混乱。

8
因此,您可以在询问的同时回答自己的问题。这是一个有趣的时间机器;)
Meer Borg

11
@MeerBorg-当您提出问题时,寻找“回答您自己的问题” 复选框-blog.stackoverflow.com/2011/07/…–
克里斯·

@eestartup-我认为您无法为自己的答案投票。我可以解释一下代码吗?还没!但是它确实适用于cygwin。
克里斯·斯诺

3
请注意:这不适用于Bash的某些配置。我相信Debian会从其Bash发行版中配置此功能。

1
抱歉,虽然这是一个不错的技巧,但它很容易导致下载损坏。while read像这样的话,就浪费了反斜杠和领先的空格,而Bash不能很好地处理NUL字节,因此二进制文件不可用。且未引用的单词$line会出现问题。答案中没有提到任何这些。
ilkkachu

19

使用山猫。

对于大多数Unix / Linux来说,这是很常见的。

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

-dump:将第一个文件转储到stdout并退出

man lynx

或netcat:

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

或telnet:

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

5
OP具有“ * nix,它没有用于下载文件的任何命令行实用程序”,因此肯定没有lynx。
Celada

2
注意lynx -source更接近wget
Steven Penny

嘿,这是一个很晚的评论,但是如何将telnet命令的输出保存到文件中?使用“>”重定向将同时输出文件的内容和telnet输出,例如“ Trying 93.184.216.34 ... Connected to www.example.com”。我处于只能使用telnet的情况下,我正在尝试使用尽可能少的框架使chroot入狱。
pixelomer

10

改编自Chris Snow答案这也可以处理二进制传输文件

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>&-
}
  • 我打破&&猫摆脱阅读
  • 我使用http 1.0,所以不需要等待/发送连接:关闭

您可以像这样测试二进制文件

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

这将无法处理二进制传输文件-它将失败为空字节。
通配符'18

@Wildcard,我不明白,我已经编辑了一个二进制文件传输示例(包含空字节),您能指出我所缺少的内容吗?
131

2
@Wildcard,嘿,是的,它看起来应该可以工作,因为它使用读取了实际的文件数据cat。我不确定这是作弊(因为它不是纯粹的外壳)还是好的解决方案(cat毕竟,因为它是标准工具)。但是@ 131,您可能要添加一条注释,说明为什么它比此处的其他解决方案更好。
ilkkachu

@Wildcard,我也在下面添加了纯bash解决方案作为答案。是的,无论作弊与否,这都是一个有效的解决方案,值得worth

7

严格地讲“ 仅Bash,别无其他 ”,这是对较早答案(@ Chris's@ 131's)的一种改编,它不调用任何外部实用程序(甚至不是标准的实用程序),但也可以处理二进制文件:

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

与一起使用download http://path/to/file > file

我们用处理NUL字节read -d ''。它一直读取到一个NUL字节,如果找到一个,则返回true,否则,返回false。Bash无法处理字符串中的NUL字节,因此当read返回true时,我们在打印时手动添加NUL字节,当它返回false时,我们知道不再有NUL字节了,这应该是最后一个数据。

使用Bash 4.4在中间有NUL,结尾为零,一个或两个NUL以及Debian中的wgetcurl二进制文件的文件上进行了测试。373 kB wget二进制文件的下载时间约为5.7秒。速度约为65 kB / s或略大于512 kb / s。

相比之下,@ 131的猫解决方案可以在不到0.1秒的时间内完成,或者快将近一百倍。确实不很奇怪。

这显然很愚蠢,因为如果不使用外部实用程序,我们对下载的文件将无能为力,甚至无法使其可执行。


是不是echo一个独立的-non shell- binary?(:p)
131

1
@ 131,不!Bash具有echoprintfas内置printfprintf -v
函数

4

如果您有这个软件包libwww-perl

您可以简单地使用:

/usr/bin/GET

考虑到其他答案不符合问题要求(仅限bash),我认为这实际上比lynx解决方案要好,因为Perl肯定比Lynx更可能预装。
马库斯

4

而是通过本地计算机上的SSH使用上传

“最小无头* nix”框表示您可能通过SSH进入了该框。因此,您也可以使用SSH 上传到它。当然,在功能上等价于(软件包等的)下载,除了当您希望将下载命令包括在无头服务器上的脚本中时。

该答案所示,您将在本地计算机上执行以下操作以将文件放置在远程无头服务器上:

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

通过SSH从第三台计算机更快地上传

与下载相比,上述解决方案的缺点是传输速度较低,因为与本地计算机的连接通常比无头服务器与其他服务器之间的连接具有更少的带宽。

为了解决这个问题,您当然可以在带宽合适的另一台服务器上执行上述命令。为了使操作更舒适(避免在第三台计算机上手动登录),以下是在本地计算机上执行的命令。

为了安全起见,请复制并粘贴包含前导空格字符的 命令' '。原因请参见以下说明。

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

说明:

  • 该命令将SSH到您的第三台计算机intermediate-host,开始通过下载文件到wget,然后开始target-host通过SSH 将其上传到。下载和上传使用您的带宽intermediate-host并且同时发生(由于Bash管道等效),因此进度会很快。

  • 使用此功能时,必须用适当的值替换两个服务器登录名(user@*-host),目标主机密码(yourpassword),下载URL(http://example.com/…)和目标主机上的输出路径(/path/to/output-file.zip)。

  • 有关使用-T -e noneSSH传输文件时的SSH选项,请参见这些详细说明

  • 该命令适用于无法使用SSH的公钥身份验证机制的情况-在某些共享托管服务提供商(尤其是Host Europe)中仍然会发生此命令。为了仍然使过程自动化,我们依靠sshpass能够在命令中提供密码。它需要sshpass安装在您的中间主机上(sudo apt-get install sshpass在Ubuntu下)。

  • 我们尝试以sshpass安全的方式使用,但是它仍然不如SSH pubkey机制安全(例如man sshpass)。特别是,我们不是通过命令行参数而是通过文件来提供SSH密码,该文件被bash进程替换所替换,以确保它在磁盘上不存在。的printf是内置在bash,确保这部分代码不弹出作为一个单独的命令ps输出,因为这将暴露出密码[ 。我认为的使用与推荐sshpasssshpass -d<file-descriptor>变体一样安全man sshpass,因为bash 始终会在内部将其映射到这样的/dev/fd/*文件描述符。而且,这无需使用临时文件[ 来源]。但是没有保证,也许我忽略了一些东西。

  • 同样,为了确保sshpass使用安全,我们需要防止将命令记录到本地计算机上的bash历史记录中。为此,整个命令以一个空格字符开头,具有此效果。

  • -o StrictHostKeyChecking=no部分可防止命令在从未连接到目标主机的情况下失败。(通常,SSH随后将等待用户输入以确认连接尝试。无论如何,我们将使其继续进行。)

  • sshpass期望sshor scp命令作为其最后一个参数。因此,我们不得不重新改写典型的wget -O - … | ssh …命令,不发一庆典管形式,如解释在这里


3

基于@Chris Snow配方。我做了一些改进:

  • http方案检查(仅支持http)
  • http响应验证(响应状态行检查,并按'\ r \ n'行而不是'Connection:close'拆分标题和正文,有时这是不正确的)
  • 非200代码失败(在Internet上下载文件很重要)

这是代码:

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

不错的增强+1
克里斯·斯诺

它起作用了,但是我发现了一个问题,当我使用此脚本时,当所有数据读取完毕时,它会等待几秒钟,这种情况不会在@Chris Snow答案中发生,有人可以解释吗?
zw963 '17

而且,在这个答案,echo -en "GET ${PATH} HTTP/1.1\r\nHost: ${HOST}\r\n${tag}\r\n\r\n" >&3${tag}未指定。
zw963 '17

我用tag变量设置了正确的答案,现在可以正常工作了。
zw963 '17

不适用于zsh,__ wget google.com,对不起,仅支持http / usr / bin / env:bash:无此类文件或目录
vrkansagara
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.