como baixar um arquivo usando apenas bash e nada mais (sem curl, wget, perl, etc.)

40

Eu tenho um * nix sem cabeça mínimo, que não possui nenhum utilitário de linha de comando para baixar arquivos (por exemplo, sem curl, wget, etc.). Eu só tenho festança.

Como posso baixar um arquivo?

Idealmente, eu gostaria de uma solução que funcionasse em uma ampla gama de * nix.

Chris Snow
fonte
que tal #gawk
Neil McGuigan
Não me lembro agora se gawk estava disponível, embora eu adoraria ver uma solução baseada gawk se você tem um :)
Chris Neve
11
Aqui está um exemplo: gnu.org/software/gawk/manual/gawkinet/gawkinet.html#Web-page
Neil McGuigan

Respostas:

64

Se você tiver o bash 2.04 ou superior com o /dev/tcppseudo-dispositivo ativado, poderá baixar um arquivo do próprio bash.

Cole o seguinte código diretamente em um shell bash (você não precisa salvar o código em um arquivo para execução):

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

Em seguida, você pode executá-lo a partir do shell, da seguinte maneira:

__wget http://example.iana.org/

Fonte: resposta da Moreaki atualizando e instalando pacotes através da linha de comando cygwin?

Atualização: conforme mencionado no comentário, a abordagem descrita acima é simplista:

  • o readwill retira as barras invertidas e os espaços em branco à esquerda.
  • O Bash não pode lidar muito bem com NUL bytes, portanto os arquivos binários estão fora.
  • sem aspas $lineirá glob.
Chris Snow
fonte
8
Então você respondeu sua própria pergunta ao mesmo tempo em que a fez. Essa é uma máquina do tempo interessante;);
Meer Borg
11
@MeerBorg - quando você faz uma pergunta, olhar para a caixa de seleção 'responder à sua própria pergunta' - blog.stackoverflow.com/2011/07/...
Chris Neve
@eestartup - Acho que você não pode votar em sua própria resposta. Posso explicar o código? Ainda não! Mas funciona no cygwin.
22713 Chris Snow
3
Apenas uma observação: isso não funcionará com algumas configurações do Bash. Eu acredito que o Debian configura esse recurso a partir de sua distribuição do Bash.
11
Urgh, embora este seja um bom truque, ele pode facilmente causar downloads corrompidos. while readdesse jeito, retira as barras invertidas e os espaços em branco à esquerda, e o Bash não consegue lidar com NUL bytes muito bem; portanto, os arquivos binários estão fora. E sem aspas $lineirá glob ... Nada disso eu vejo mencionado na resposta.
Ilkkachu 16/05/19
19

Use lince.

É bastante comum para a maioria do Unix / Linux.

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

-dump: despeja o primeiro arquivo no stdout e sai

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
pilha de lenha
fonte
5
O OP possui "* nix, que não possui utilitários de linha de comando para baixar arquivos", portanto não há lince com certeza.
Celada
2
Nota lynx -sourceestá mais perto de wget
Steven Penny
Ei, então esse é um comentário muito tarde, mas como você salva a saída do comando telnet em um arquivo? Redirecionar com ">" gera o conteúdo do arquivo e a saída de telnet, como "Tentando 93.184.216.34 ... Conectado ao www.example.com.". Estou em uma situação em que só posso usar o telnet, estou tentando fazer uma prisão chroot com o mínimo de estruturas possível.
pixelomer
10

Adaptado da resposta de Chris Snow Isso também pode lidar com arquivos de transferência binária

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>&-
}
  • eu quebro o gato para sair da leitura
  • eu uso o http 1.0, então não há necessidade de esperar / enviar uma conexão: fechar

Você pode testar arquivos binários como este

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
fonte
Isso não processa arquivos de transferência binários - falhará em bytes nulos.
Curinga
@ Wildcard, eu não entendo, eu editei com um exemplo de transferência de arquivo binário (contendo bytes nulos), você pode me indicar o que está faltando?
131
2
@ Wildcard, heheh, sim, parece que deve funcionar, pois lê os dados reais do arquivo cat. Não tenho certeza se isso é trapaça (já que não é puramente o shell) ou uma solução agradável (já que caté uma ferramenta padrão, afinal). Mas @ 131, você pode adicionar uma observação sobre por que funciona melhor do que as outras soluções aqui.
Ilkkachu
@ Wildcard, eu adicionei a solução pura do bash também como resposta abaixo. E sim, traindo ou não, esta é uma solução válida e merece um upvote :)
ilkkachu
7

Tomando estritamente o " just Bash e nada mais ", aqui está uma adaptação das respostas anteriores ( @ Chris , @ 131 ) que não chama nenhum utilitário externo (nem mesmo o padrão), mas também funciona com arquivos binários:

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

Use com download http://path/to/file > file.

Lidamos com NUL bytes com read -d ''. Ele lê até um byte NUL e retorna true se encontrou um, false se não encontrou. O Bash não pode manipular NUL bytes em cadeias de caracteres, portanto, quando readretorna com true, adicionamos o byte NUL manualmente ao imprimir e, quando retorna false, sabemos que não há mais bytes NUL e esse deve ser o último dado .

Testado com o Bash 4.4 em arquivos com NULs no meio e terminando em zero, um ou dois NULs, e também com os binários wgete curldo Debian. O wgetbinário de 373 kB levou cerca de 5,7 segundos para baixar. Uma velocidade de cerca de 65 kB / s ou um pouco mais que 512 kb / s.

Em comparação, a solução de gato do @ 131 termina em menos de 0,1 s, ou quase cem vezes mais rápido. Não é muito surpreendente, realmente.

Isso é obviamente tolo, já que sem o uso de utilitários externos, não há muito o que fazer com o arquivo baixado, nem torná-lo executável.

ilkkachu
fonte
O eco não é um binário independente, não shell? (: p)
131
11
@ 131, não! Bash tem echoe printfcomo builtins (ele precisa de um embutido printfpara implementar printf -v)
ilkkachu
4

Se você possui este pacote libwww-perl

Você pode simplesmente usar:

/usr/bin/GET
stackexchanger
fonte
Considerando que outras respostas não respeitam o requisito de pergunta (somente bash), acho que isso é realmente melhor que a lynxsolução, pois o Perl certamente tem mais chances de ser pré-instalado que o Lynx.
Marcus
4

Em vez disso, use o upload via SSH da sua máquina local

Uma caixa "mínimo sem cabeça * nix" significa que você provavelmente faz o SSH nela. Então você também pode usar o SSH para fazer o upload para ele. O que é funcionalmente equivalente ao download (de pacotes de software etc.), exceto quando você deseja que um comando de download inclua em um script no servidor sem cabeçalho, é claro.

Conforme mostrado nesta resposta , você executaria o seguinte em sua máquina local para colocar um arquivo em seu servidor sem controle remoto:

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

Upload mais rápido via SSH de uma terceira máquina

A desvantagem da solução acima, em comparação com o download, é a velocidade de transferência mais baixa, pois a conexão com a máquina local geralmente possui muito menos largura de banda do que a conexão entre o servidor sem cabeça e outros servidores.

Para resolver isso, é claro que você pode executar o comando acima em outro servidor com largura de banda decente. Para tornar isso mais confortável (evitando um login manual na terceira máquina), aqui está um comando para executar na sua máquina local .

Para garantir a segurança, copie e cole esse comando, incluindo o caractere de espaço inicial ' ' . Veja as explicações abaixo para o 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' \
"

Explicações:

  • O comando ssh para sua terceira máquina intermediate-host, começará a baixar um arquivo para lá via wgete comece a carregá-lo para target-hostvia SSH. O download e o upload usam a largura de banda intermediate-hoste acontecem ao mesmo tempo (devido aos equivalentes do canal Bash), portanto, o progresso será rápido.

  • Ao usar isso, você deve substituir os dois logins do servidor ( user@*-host), a senha do host de destino ( yourpassword), a URL de download ( http://example.com/…) e o caminho de saída no host de destino ( /path/to/output-file.zip) pelos valores próprios apropriados.

  • Para as -T -e noneopções de SSH ao usá-lo para transferir arquivos, consulte estas explicações detalhadas .

  • Este comando destina-se a casos em que você não pode usar o mecanismo de autenticação de chave pública do SSH - ainda acontece com alguns provedores de hospedagem compartilhada, principalmente a Host Europe . Para automatizar ainda o processo, contamos com sshpassa capacidade de fornecer a senha no comando. Ele precisa sshpassser instalado no seu host intermediário ( sudo apt-get install sshpassno Ubuntu).

  • Tentamos usar de sshpassmaneira segura, mas ainda não será tão seguro quanto o mecanismo pubkey SSH (diz man sshpass). Em particular, fornecemos a senha SSH não como argumento de linha de comando, mas por meio de um arquivo que é substituído pela substituição do processo do bash para garantir que ela nunca exista no disco. O printfé um bash embutido, certificando-se de que essa parte do código não apareça como um comando separado na pssaída, pois isso exporia a senha [ origem ]. Eu acho que esse uso de sshpassé tão seguro quanto a sshpass -d<file-descriptor>variante recomendada man sshpass, porque o bash o mapeia internamente para um /dev/fd/*descritor de arquivo assim. E isso sem usar um arquivo temporário [ fonte] Mas não há garantias, talvez eu tenha esquecido alguma coisa.

  • Novamente, para tornar o sshpassuso seguro, precisamos impedir que o comando seja gravado no histórico do bash em sua máquina local. Para isso, todo o comando é anexado com um caractere de espaço, que tem esse efeito.

  • A -o StrictHostKeyChecking=noparte evita que o comando falhe, caso nunca se conecte ao host de destino. (Normalmente, o SSH aguardaria a entrada do usuário para confirmar a tentativa de conexão. Fazemos o procedimento de qualquer maneira.)

  • sshpassespera um comando sshou scpcomo seu último argumento. Portanto, temos que reescrever o wget -O - … | ssh …comando típico em um formulário sem um pipe do bash, conforme explicado aqui .

tanius
fonte
3

Com base na receita de Chris Snow. Fiz algumas melhorias:

  • verificação de esquema http (suporta apenas http)
  • validação de resposta http (verificação da linha de status da resposta e dividir o cabeçalho e o corpo pela linha '\ r \ n', não 'Connection: close', que às vezes não é verdade)
  • falhou no código que não é 200 (é importante fazer o download de arquivos na internet)

Aqui está o código:

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
fonte
Aprimoramentos agradáveis ​​+1
Chris Snow
Funcionou, mas encontrei uma preocupação, quando uso esses scripts. Aguarde alguns segundos quando todos os dados são lidos, esse caso não ocorre na resposta do Chris Snow, alguém poderia explicar isso?
Zw963
E, nesta resposta, echo -en "GET ${PATH} HTTP/1.1\r\nHost: ${HOST}\r\n${tag}\r\n\r\n" >&3, ${tag}não é especificada.
Zw963 19/05
Eu edito esta resposta com a tagvariável está correta, funciona bem agora.
Zw963
não está funcionando com o zsh, __wget google.com desculpe, suporte apenas http / usr / bin / env: bash: Esse arquivo ou diretório não
existe #