как скачать файл, используя только bash и ничего больше (без curl, wget, perl и т. д.)

40

У меня есть минимальный обезглавленный * nix, который не имеет никаких утилит командной строки для загрузки файлов (например, без curl, wget и т. Д.). У меня есть только Bash.

Как я могу скачать файл?

В идеале мне бы хотелось, чтобы решение работало с широким спектром * nix.

Крис Сноу
источник
как насчетgawk
Нил Макгиган
Сейчас я не могу вспомнить, был ли gawk доступен, хотя я бы хотел увидеть решение на основе gawk, если оно у вас есть :)
Крис Сноу,
1
Вот пример: gnu.org/software/gawk/manual/gawkinet/gawkinet.html#Web-page
Нил Макгиган

Ответы:

64

Если у вас установлен bash 2.04 или выше с /dev/tcpвключенным псевдоустройством, вы можете загрузить файл из самого bash.

Вставьте следующий код непосредственно в оболочку bash (вам не нужно сохранять код в файле для выполнения):

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/

Источник: ответ Мореаки об обновлении и установке пакетов через командную строку cygwin?

Обновление: как упомянуто в комментарии, изложенный выше подход упрощен:

  • readволя громит и обратные слэши ведущие пробелы.
  • Bash не очень хорошо справляется с байтами NUL, поэтому двоичные файлы отсутствуют.
  • без кавычек $lineбудет шар.
Крис Сноу
источник
8
Таким образом, вы ответили на свой вопрос в то же время, когда вы его задали. Это интересная машина времени у вас есть;)
Меер Борг
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: выгрузить первый файл в стандартный вывод и выйти

man lynx

Или netcat:

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

Или телнет:

(echo 'GET /'; echo ""; sleep 1; ) | telnet www.google.com 80
woodstack
источник
5
У OP есть «* nix, который не имеет никаких утилит командной строки для загрузки файлов», поэтому lynx точно не существует.
Селада
2
Примечание lynx -sourceближе к Wget
Стивен Пенни
Эй, это действительно поздний комментарий, но как сохранить вывод команды telnet в файл? При перенаправлении с «>» выводится как содержимое файла, так и вывод telnet, например «Попытка 93.184.216.34 ... подключена к www.example.com.». Я нахожусь в ситуации, когда я могу использовать только telnet, я пытаюсь сделать chroot-джейл с минимально возможными фреймворками.
пикселомер
10

Адаптировано из ответа Криса Сноу. Это также может обрабатывать двоичные файлы передачи.

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
131
источник
Это не будет обрабатывать двоичные файлы передачи - оно завершится с ошибкой в ​​нулевых байтах.
Wildcard
@Wildcard, я не понимаю, я отредактировал пример передачи двоичного файла (содержащий нулевые байты), можете ли вы указать мне, что я пропускаю?
131
2
@Wildcard, хе-хе, да, похоже, он должен работать, так как он читает фактические данные файла с cat. Я не уверен, что это обман (так как это не просто оболочка) или хорошее решение (так catкак это стандартный инструмент, в конце концов). Но @ 131, вы можете добавить примечание о том, почему он работает лучше, чем другие решения здесь.
ilkkachu
@Wildcard, я добавил чистое решение Bash, как ответ ниже. И да, измена или нет, это верное решение и стоит упредить :)
ilkkachu
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 на файлах с NULs в середине, и заканчивается в ноль, один или два NULs, а также с wgetи curlдвоичные файлы из Debian. wgetЗагрузка двоичного файла объемом 373 КБ заняла около 5,7 секунд. Скорость около 65 кБ / с или чуть больше 512 кб / с.

Для сравнения, решение @ 131 для cat завершается менее чем за 0,1 с, или почти в сто раз быстрее. Не очень удивительно, правда.

Это, очевидно, глупо, поскольку без использования внешних утилит мы мало что можем сделать с загруженным файлом, даже не сделать его исполняемым.

ilkkachu
источник
Разве эхо не является изолированным двоичным файлом? (: p)
131
1
@ 131 нет! Bash имеет echoи в printfкачестве встроенных команд (требуется встроенная команда printfдля реализации printf -v)
ilkkachu
4

Если у вас есть этот пакет libwww-perl

Вы можете просто использовать:

/usr/bin/GET
stackexchanger
источник
Учитывая, что другие ответы не соответствуют требованию вопроса (только 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 noneвариантов SSH при использовании его для передачи файлов, см этих подробных объяснений .

  • Эта команда предназначена для случаев, когда вы не можете использовать механизм аутентификации с открытым ключом SSH - это все еще происходит с некоторыми провайдерами общего хостинга, особенно Host Europe . Чтобы по-прежнему автоматизировать процесс, мы полагаемся на sshpassвозможность ввести пароль в команде. Он sshpassдолжен быть установлен на вашем промежуточном хосте ( sudo apt-get install sshpassпод Ubuntu).

  • Мы пытаемся использовать sshpassбезопасный способ, но он все еще не будет таким безопасным, как механизм SSH pubkey (говорит man sshpass). В частности, мы предоставляем пароль SSH не в качестве аргумента командной строки, а через файл, который заменяется заменой процесса bash, чтобы убедиться, что он никогда не существует на диске. Это printfвстроенный bash, гарантирующий, что эта часть кода не psбудет отображаться как отдельная команда при выводе, так как это предоставит пароль [ source ]. Я думаю, что это использование sshpassтак же безопасно, как и sshpass -d<file-descriptor>рекомендуемый вариант man sshpass, потому что bash в /dev/fd/*любом случае сопоставляет его внутренне с таким файловым дескриптором. И это без использования временного файла [ источник]. Но никаких гарантий, может я что-то упустил.

  • Опять же, чтобы сделать sshpassиспользование безопасным, нам нужно запретить запись команды в историю bash на вашем локальном компьютере. Для этого всей команде добавляется один пробел, который имеет этот эффект.

  • Эта -o StrictHostKeyChecking=noчасть предотвращает сбой команды в случае, если она никогда не подключалась к целевому хосту. (Обычно SSH будет ожидать ввода данных от пользователя для подтверждения попытки подключения. Мы все равно продолжаем.)

  • sshpassожидает команду sshили в scpкачестве последнего аргумента. Таким образом, мы должны переписать типичную wget -O - … | ssh …команду в форму без канала bash, как описано здесь .

tanius
источник
3

По рецепту @Chris Snow. Я сделал некоторые улучшения:

  • Проверка схемы http (поддерживает только http)
  • проверка ответа http (проверка строки состояния ответа и разделение заголовка и тела по строке '\ r \ n', а не 'Connection: close', что иногда неверно)
  • ошибка кода не-200 (важно скачивать файлы в интернете)

Вот код:

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
И в этом ответе, echo -en "GET ${PATH} HTTP/1.1\r\nHost: ${HOST}\r\n${tag}\r\n\r\n" >&3, ${tag}не уточняется.
zw963
Я редактирую этот ответ с tagпеременной, правильно установлен, теперь он работает хорошо.
zw963
не работает с zsh, __wget google.com извините, только поддержка http / usr / bin / env: bash: Нет такого файла или каталога
vrkansagara