як завантажити файл за допомогою просто bash та нічого іншого (без curl, wget, perl тощо)


40

У мене мінімальний безголовий * nix, який не має жодних утиліт командного рядка для завантаження файлів (наприклад, без curl, wget тощо). У мене тільки баш.

Як я можу завантажити файл?

В ідеалі я хотів би отримати рішення, яке б працювало в широкому діапазоні * nix.


як щодоgawk
Ніл МакГуйган

Не можу зараз пригадати, чи був доступний gawk, хоч я хотів би побачити рішення, яке базується на gawk, якщо у вас воно є :)
Chris Snow

Відповіді:


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
Отже, ви відповіли на власне запитання одночасно, коли ви його задали. Це цікава машина часу у вас;)
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

Або сітка:

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

Або telnet:

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

5
В ОП є "* nix, який не має жодних утиліт командного рядка для завантаження файлів", тому жоден рись точно.
Селада,

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>&-
}
  • я перерваю & & cat, щоб вийти з прочитаного
  • я використовую 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

Це не обробляє файли бінарних передач - це не вдасться до нульових байтів.
Wildcard

@Wildcard, я не розумію, я редагував приклад передачі бінарних файлів (містить нульові байти), чи можете ви вказати мені, що мені не вистачає?
131

2
@Wildcard, heheh, так, схоже, це має працювати, оскільки він читає фактичні дані файлів за допомогою cat. Я не впевнений, чи це обман (оскільки це не суто оболонка), чи приємне рішення (оскільки cat, зрештою, це стандартний інструмент). Але @ 131, ви можете додати примітку про те, чому це працює краще, ніж інші рішення тут.
ilkkachu

@Wildcard, я також додав чистий баш-розчин як відповідь нижче. І так, обман чи ні, це правильне рішення і варто підняти нагору :)
ilkkachu

7

Якщо чітко взяти " просто Баш і нічого іншого ", ось одна адаптація попередніх відповідей ( @ 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 вручну під час друку, а коли він повертає помилкове значення, ми знаємо, що більше немає байтів NUL, і це має бути останній фрагмент даних .

Тестується з Bash 4.4 на файлах з NUL в середині, і закінчуючи нулем, однією або двома NUL, а також з wgetі бінарними файлами curlвід Debian. Для завантаження wgetдвійкового файлу 373 кБ було потрібно близько 5,7 секунди. Швидкість близько 65 кБ / с або трохи більше 512 кб / с.

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

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


Чи не відлуння окремої -не оболонки - бінарної? (: p)
131

1
@ 131, ні! У Bash є echoі printfяк вбудований (для його printfвтілення потрібен вбудований printf -v)
ilkkachu

4

Якщо у вас є цей пакет libwww-perl

Ви можете просто використовувати:

/usr/bin/GET

Зважаючи на те, що інші відповіді не відповідають вимозі запитання (лише башти), я думаю, що це насправді краще, ніж 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 pipe), тому прогрес буде швидким.

  • Використовуючи це, вам потрібно замінити два входи сервера ( user@*-host), цільовий пароль хоста ( yourpassword), URL-адресу завантаження ( http://example.com/…) та вихідний шлях на ваш цільовий хост ( /path/to/output-file.zip) на відповідні власні значення.

  • Про параметри -T -e noneSSH при використанні його для передачі файлів див. У цих детальних поясненнях .

  • Ця команда призначена для випадків, коли ви не можете використовувати механізм автентифікації відкритого ключа SSH - це все ще відбувається з деякими постачальниками послуг спільного хостингу, зокрема з Host Europe . Щоб все-таки автоматизувати процес, ми розраховуємо на sshpassте, що зможемо надати пароль у команді. Його потрібно sshpassвстановити на проміжному хості ( sudo apt-get install sshpassпід Ubuntu).

  • Ми намагаємося використовувати sshpassзахищений спосіб, але він все ще не буде настільки захищеним, як механізм SSkey-порту (каже man sshpass). Зокрема, ми подаємо пароль SSH не як аргумент командного рядка, а через файл, який замінюється на заміну процесу bash, щоб переконатися, що він ніколи не існує на диску. Це printfвбудований bash, переконайтеся, що ця частина коду не з'являється як окрема команда у psвиводі, оскільки це відкриє пароль [ джерело ]. Я думаю, що це використання sshpassнастільки ж безпечне, як і sshpass -d<file-descriptor>рекомендований варіант man sshpass, тому що bash відображає його всередині такого /dev/fd/*дескриптора файлів. І це без використання тимчасового файлу [ джерело]. Але жодних гарантій, можливо, я щось пропустив.

  • Знову ж таки, щоб зробити sshpassбезпеку використання нам потрібно не допустити, щоб команда записувалася до історії башів на вашій локальній машині. Для цього вся команда претендує на один пробіл, який має такий ефект.

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

  • sshpassочікує, що sshабо scpкоманда є останнім аргументом. Отже, нам доведеться переписати типову wget -O - … | ssh …команду у форму без bash pipe, як пояснено тут .


3

На основі рецепта @Chris Snow Я вніс кілька покращень:

  • Перевірка схеми http (вона підтримує лише http)
  • Перевірка відповіді http (перевірка рядка статусу відповіді та розділений заголовок та тіло на рядок "\ r \ n", а не "З'єднання: закрити", що не відповідає дійсності)
  • не вдалося отримати код не 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
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.