Skip to content
Français
Account: not signed in, sync available My learning
Select theme
Français

wget: download from the command line

By updated

Improve this page

wget fetches a file from an HTTP, HTTPS or FTP server without any interaction. You give it a URL, it writes the file to disk, and it can resume an interrupted transfer, limit its bandwidth and mirror a whole site. It is the tool you call in an installation script, a cron job or a build container: it runs without a terminal, without a keyboard and without supervision.

This page is a reference: each option is presented with the moment it is really useful in administration. If you would rather query a REST API, read headers or send JSON, curl is what you need. The rule fits in one sentence: wget downloads, curl talks.

The options described here are those of GNU Wget 1.21.4, the version shipped with Ubuntu 24.04, and the examples were replayed with Wget 1.25.0 on Debian 13 and 1.24.5 on AlmaLinux 10. An option missing on an older machine is checked with wget --version.

What you will learn

  • Download a file, rename it and resume an interrupted transfer.
  • Script wget cleanly: silence, log, timeouts, retries, return codes.
  • Fetch several files from a list or a numbered series.
  • Mirror a site while controlling depth, domains and file types.
  • Authenticate with a password, a token or a cookie, and go through a proxy.
  • Avoid the classic pitfalls: overwriting, no globbing in HTTP, -q hiding errors.

Installing wget

wget is present by default on most server distributions, but not in every minimal container image, where sometimes only curl is installed. The reflex before building a script on it: check that it is there with wget --version, or watch the build break in production.

Terminal window
sudo apt install wget

Syntax and essential options

The command always takes the same form: options, then one or more URLs. Options can go before or after the URL, but grouping them before remains much more readable in a script.

Terminal window
wget [options] URL...

Here are the ten options you will really use day to day. Each one is detailed below with its context of use: a reference page is useless if it does not say when to take the option out of the drawer.

OptionRoleWhen it is useful
-O fichierWrites to the given fileForce a name, or write to /dev/null
-cResumes a partial transferISO or dump cut off midway
-ncDoes not rewrite an existing fileScript replayed several times
-q / -nvTotal silence / condensed outputCron, CI, clean logs
-i liste.txtReads the URLs from a fileBatch of packages or artifacts
-P dossierPuts the files in a directoryAvoid cluttering the current directory
--limit-rate=RATECaps the bandwidthDo not saturate a shared link
--tries=N / --timeout=SRetries and timeoutsMake a script predictable
--spiderTests without downloadingCheck that a URL still answers
-mMirror of a siteBackup of a documentation

Downloading a file

Without any option, wget saves the file under the last segment of the URL, in the current directory, and shows a progress bar. It is by far the most frequent use case: fetching an ISO image, a package or a release binary.

Terminal window
wget https://cdimage.debian.org/debian-cd/current/amd64/iso-cd/SHA256SUMS

The SHA256SUMS file appears in the current directory. Always check with ls -l that the size is not zero: a server that returns an HTML error page produces a file that is indeed present, but unusable.

Choosing the file name with -O

The -O option (capital letter O, for output document) forces the local name. It is essential when the URL ends with an unreadable identifier, and when you want to throw the content away to /dev/null and keep only the return code.

Terminal window
wget -O jq https://github.com/jqlang/jq/releases/download/jq-1.7.1/jq-linux-amd64
chmod +x jq

Two useful variants avoid guessing the name. --content-disposition honours the name announced by the server in the Content-Disposition header, which is what GitHub releases do behind their redirect URLs:

Terminal window
wget --content-disposition https://github.com/jqlang/jq/releases/download/jq-1.7.1/jq-linux-amd64
# does write: jq-linux-amd64

--trust-server-names names the file after the final URL after redirection, and not after the one you typed. On a short link that redirects to installeur-2.14.tar.gz, the difference is very concrete: without the option, wget writes a file carrying the starting name, often unreadable.

Not overwriting an existing file

Contrary to a widespread intuition, wget does not overwrite a file with the same name: it creates SHA256SUMS.1, then SHA256SUMS.2. In a script replayed every day, you therefore pile up numbered duplicates that fill the disk without any message.

The -nc option (no-clobber) cuts this short: if the file is already present, wget downloads nothing, renumbers nothing and moves on to the next one.

Terminal window
wget -nc https://cdimage.debian.org/debian-cd/current/amd64/iso-cd/SHA256SUMS
# File ‘SHA256SUMS’ already there; not retrieving.

It is the right reflex for a local cache of artifacts. Be careful, though: -nc checks neither the date nor the content. If the remote file changed, you keep the old one. To refresh based on the date, use -N (timestamping), which downloads again only if the remote copy is newer than the local one.

Resuming an interrupted transfer with -c

On a 4 GB ISO cut off at 80%, restarting wget from zero is a waste of time. The -c option (continue) sends a Range header to the server, which answers 206 Partial Content and resumes at the exact byte where the transfer had stopped.

Terminal window
wget -c https://releases.ubuntu.com/24.04/ubuntu-24.04-live-server-amd64.iso

Resuming is only possible if the server accepts partial requests. If it ignores them, wget starts again from the beginning: that is the expected behaviour, not a bug. And if the local file is larger than the remote one, wget considers that there is nothing to resume.

Scripting wget cleanly

A wget run by hand can afford to be chatty. In a cron job, a CI pipeline or a Dockerfile, it must be silent, bounded in time and verifiable. These four settings make the difference between a reliable script and a script that hangs forever on a Friday evening.

Silence, log and background

-q (quiet) removes all output, errors included. -nv (no-verbose) removes the progress bar but keeps the error messages and one summary line per file: it is the best compromise for a CI log.

Terminal window
wget -nv -O /tmp/SHA256SUMS https://cdimage.debian.org/debian-cd/current/amd64/iso-cd/SHA256SUMS

For a very large transfer started from an SSH session, -b detaches wget into the background and -o writes the progress into a log file. The session can then close without interrupting the download.

Terminal window
wget -b -o /var/log/iso-ubuntu.log https://releases.ubuntu.com/24.04/ubuntu-24.04-live-server-amd64.iso

Checking the return code

It is the most often forgotten point. With -q, a 404 error produces no message: the script carries on as if all was well, and fails three steps later on an empty file. Only the return code tells the truth.

Terminal window
wget -q -O paquet.deb https://depot.interne.lan/paquets/agent_2.4.1_amd64.deb
echo "code de retour : $?"
CodeObserved meaningTypical cause
0Download succeededNothing to do
4Network failureDNS not found, host unreachable
8The server answered with an error404, 403, 500

Hence the reflex to anchor in every script: test the return code, or let set -e stop the execution. An unchecked download is a silent failure waiting to happen.

Terminal window
if ! wget -nv -O paquet.deb https://depot.interne.lan/paquets/agent_2.4.1_amd64.deb; then
echo "Téléchargement du paquet impossible, on arrête là." >&2
exit 1
fi

Bounding timeouts and retries

By default, wget retries 20 times, which can make a script hang for a very long time on a server that does not answer. Two options restore order: --tries sets the number of attempts, --timeout sets the timeouts.

Terminal window
wget --tries=2 --timeout=10 -O agent.deb https://depot.interne.lan/paquets/agent_2.4.1_amd64.deb

Remember the exact scope of --timeout: it sets the three timeouts at once, namely DNS resolution, connection establishment and data reading. If you need finer control, the three options exist separately:

  • --dns-timeout=SEC: name resolution timeout.
  • --connect-timeout=SEC: TCP connection establishment timeout.
  • --read-timeout=SEC: idle timeout during the transfer.

On an unreachable host, --tries=2 --timeout=3 gives control back in a few seconds (7 s measured) instead of several minutes. It is exactly what you want in a healthcheck or a scheduled task.

Capping the bandwidth

A wget started on a large ISO readily saturates the site's shared link and makes everyone suffer. --limit-rate caps the bandwidth, in k (kilobytes) or m (megabytes) per second.

Terminal window
wget --limit-rate=500k https://releases.ubuntu.com/24.04/ubuntu-24.04-live-server-amd64.iso

The effect is immediate and measurable: 5 MiB capped at 500 k/s take 10 seconds instead of being swallowed instantly. It is the right way to fetch a large image during business hours without degrading the shared line.

Testing a URL without downloading it

--spider sends the request, reads the response and writes nothing to disk. Combined with the return code, it gives a one-line URL probe, perfect to check that an internal mirror still answers or that a documentation link is not dead.

Terminal window
wget --spider -q https://depot.interne.lan/paquets/agent_2.4.1_amd64.deb
echo $? # 0 : l'URL répond ; 8 : le serveur renvoie une erreur

Add -S (server response) to print the HTTP headers returned and diagnose a looping redirect or a misconfigured cache.

Downloading several files

Three approaches, depending on the shape of your need: a known list of URLs, a numbered series, or a richer logic that calls for a shell loop.

From a list of URLs

The -i option reads a text file containing one URL per line. It is the most robust method to fetch a batch of packages or artifacts, because the list is versionable and can be reviewed before execution.

Terminal window
cat > paquets.txt <<'EOF'
https://cdimage.debian.org/debian-cd/current/amd64/iso-cd/SHA256SUMS
https://cdimage.debian.org/debian-cd/current/amd64/iso-cd/SHA512SUMS
EOF
wget -nv -i paquets.txt -P /srv/miroir/

-P puts everything in the given destination directory, without cluttering the current directory. Add -c to replay the list after an interruption without starting from zero.

A series of numbered URLs

It is a frequent confusion: wget does not generate URLs from a pattern in HTTP. Writing wget https://serveur/rapport-0[1-5].csv literally asks the server for the file named rapport-0[1-5].csv, which answers with a 404 error. The bracket pattern is curl syntax, not wget's.

The solution with wget is to let the shell generate the URLs, thanks to Bash brace expansion. The braces are expanded before the call, and wget therefore receives several distinct URLs:

Terminal window
wget https://depot.interne.lan/rapports/rapport-0{1..5}.csv

Bash also handles leading zeros and increment steps, which covers almost every real need, such as a numbered series of dumps:

Terminal window
wget https://depot.interne.lan/sauvegardes/dump-{01..10}.sql.gz

With curl, the bracket pattern works natively, and -O keeps the remote name of each file:

Terminal window
curl -O "https://depot.interne.lan/rapports/rapport-0[1-5].csv"

With a shell loop

As soon as you need to rename, filter or test between two downloads, the loop takes the lead again. It also makes the script more readable than a wget command overloaded with options.

Terminal window
while read -r url; do
wget -nc -nv -P /srv/miroir/ "$url" || echo "Échec : $url" >> /tmp/echecs.txt
done < paquets.txt

The quotes around $url are not decorative: without them, a URL containing & or a space breaks the command. wget is not parallel: to speed things up, start several instances in the background with & then wait, keeping in mind that you multiply the load on the remote server.

Mirroring a site in recursive mode

Recursive mode follows the links contained in HTML pages and downloads what it finds. It is the function that really sets wget apart from curl: it serves to archive a documentation before a service is shut down, or to keep an offline copy of an internal site.

Turning on recursion

-r turns on link following, -m (mirror) is the ready-to-use shortcut. The manual is explicit: -m is equivalent to -r -N -l inf --no-remove-listing, so infinite depth, update by date and keeping FTP listings.

Terminal window
wget -m -k -p -np -nH https://docs.interne.lan/runbooks/

This combination is the one actually used for archiving, and each of its four options fixes a precise weakness of a raw mirror:

  • -k (--convert-links) rewrites the links so that they point to the local files, without which the copy cannot be browsed offline.
  • -p (--page-requisites) fetches the images, CSS and scripts needed for display.
  • -np (--no-parent) forbids going up above the starting directory.
  • -nH (--no-host-directories) avoids creating a directory named after the server.

A clarification that avoids much misunderstanding: wget only discovers the files really linked from the pages walked. A file present on the server but that no link references will never be fetched: recursive mode is not a directory browser.

Controlling depth and domains

By default, the recursion depth is 5 levels. -l changes it, -l inf makes it infinite. A poorly bounded mirror can download gigabytes: always start small, then widen.

Terminal window
wget -r -l 2 -np https://docs.interne.lan/runbooks/

For safety, wget stays on the starting host. To widen, -H (--span-hosts) allows other hosts, and is then bounded with -D (--domains) or --exclude-domains. The trio reads as an allow list and a deny list of domains.

Terminal window
wget -r -l 2 -H -D docs.interne.lan,cdn.interne.lan https://docs.interne.lan/runbooks/

Filtering the files fetched

-A (accept) and -R (reject) take a list of comma-separated extensions, which is enough in most cases. To target a URL pattern, --accept-regex and --reject-regex accept a regular expression.

Terminal window
# Ne récupérer que les PDF et les archives d'un dépôt interne
wget -r -l 2 -np -A pdf,tar.gz https://docs.interne.lan/livrables/
# Ignorer les URL contenant un identifiant de session
wget -r -np --reject-regex ".*\?sessionid=.*" https://docs.interne.lan/runbooks/

Add -Q (quota) to cap the total volume downloaded, for example -Q500m. It is the safeguard that avoids filling a disk overnight, and it combines with -l to bound the mirror on two axes.

Authenticating

Three mechanisms cover most of the protected resources you will meet: the HTTP password, the token in a header, and the session cookie. In all three cases, the security rule is the same: never write a secret on the command line, because it is visible to everyone through ps aux and ends up in the shell history.

HTTP password

--http-user and --http-password provide the credentials of a Basic or Digest authentication: wget picks the method announced by the server, you have nothing to force.

Terminal window
wget --http-user=deploy --ask-password https://depot.interne.lan/paquets/agent_2.4.1_amd64.deb

--ask-password asks for the password interactively, without leaving it in clear text on the command line: it is the form to prefer for manual use. For a script, prefer a ~/.netrc file in chmod 600, which wget reads automatically. The --auth-no-challenge option sends the credentials from the first request, without waiting for the server challenge; only turn it on if the server requires it, because it exposes the password even when it was not necessary.

Token in a header

Artifact repositories and APIs most often expect a Bearer token. --header adds an arbitrary HTTP header, but beware of the trap: --header="Authorization: Bearer ${DEPOT_TOKEN}" is expanded by the shell before the call, and the token ends up in clear text in the process arguments, readable through ps aux. The environment variable therefore protects nothing. The fix is to pass the header through a dedicated configuration file, in chmod 600, which --config loads instead of ~/.wgetrc:

Terminal window
umask 077 # the file created is readable by you only
printf 'header = Authorization: Bearer %s\n' "$DEPOT_TOKEN" > ~/.wgetrc-depot
wget --config="$HOME/.wgetrc-depot" \
-O agent.deb https://depot.interne.lan/api/paquets/agent-2.4.1
rm -f ~/.wgetrc-depot

Since printf is a shell builtin, it creates no process and the token never shows up in ps. Measured with wget 1.21.4: the server does receive the Authorization header. For headers without a secret, such as Accept: or a custom header required by a corporate proxy, --header remains the simplest form and can be repeated as many times as needed.

For an application that protects its downloads behind a login form, you must first obtain a cookie, then replay it. --keep-session-cookies is essential: without it, wget only saves the persistent cookies and drops the session one, precisely the one you need.

Terminal window
umask 077
printf 'user=deploy&password=%s' "$MDP" > identifiants.txt
wget --save-cookies cookies.txt --keep-session-cookies \
--post-file=identifiants.txt \
-O /dev/null https://portail.interne.lan/login
rm -f identifiants.txt
wget --load-cookies cookies.txt -O rapport.csv https://portail.interne.lan/exports/rapport.csv

--post-file reads the request body from a file, for the same reason as above: a --post-data="...${MDP}" would expose the password in ps. The cookies.txt file then contains a valid session token: treat it like a password, keep it in chmod 600 (the umask takes care of it) and delete it at the end of the script.

FTP and FTPS

wget still handles FTP, a protocol you meet on old internal mirrors or industrial equipment. The credentials go through --ftp-user and --ftp-password, and a ftps:// URL switches to the encrypted variant.

Terminal window
wget --ftp-user=archives --ask-password ftp://miroir.interne.lan/isos/debian-12.iso

Plain FTP carries the credentials in clear text over the network. If it is still used where you work, it is a point to raise: prefer ftps://, or better, HTTPS access.

Proxy and network stack

On a server that has no direct Internet access, everything goes through an outbound proxy. wget honours the standard environment variables, which makes it the simplest and most portable method.

Terminal window
export http_proxy="http://proxy.interne.lan:3128/"
export https_proxy="http://proxy.interne.lan:3128/"
export no_proxy="localhost,127.0.0.1,depot.interne.lan"
wget https://cdimage.debian.org/debian-cd/current/amd64/iso-cd/SHA256SUMS

no_proxy lists the destinations that must stay on a direct connection, typically your internal repositories. For a one-off call without touching the environment, -e injects the matching configuration directive, and --proxy-user provides the credentials if the proxy authenticates. The directive follows the scheme of the target URL: an https:// URL reads https_proxy, and an -e "http_proxy=..." would be ignored without the slightest message:

Terminal window
wget -e "https_proxy=http://proxy.interne.lan:3128" \
--proxy-user=deploy --ask-password \
https://depot.interne.lan/paquets/agent_2.4.1_amd64.deb

Conversely, --no-proxy ignores the configured proxy for this request: it is the first test to run when an internal download fails for no apparent reason. Finally, on a dual-stack network, -4 and -6 force the address family and fix many slowdowns caused by an IPv6 that is announced but not routed.

Terminal window
wget -4 https://depot.interne.lan/paquets/agent_2.4.1_amd64.deb

The .wgetrc configuration file

Repeating the same proxy and timeout options in each script is a source of error. The ~/.wgetrc file (personal configuration) holds these default values once and for all; /etc/wgetrc does the same for the whole machine. Each line is a directive, not a shell command.

~/.wgetrc
http_proxy = http://proxy.interne.lan:3128/
https_proxy = http://proxy.interne.lan:3128/
no_proxy = localhost,127.0.0.1,depot.interne.lan
dns_timeout = 5
connect_timeout = 10
read_timeout = 20
tries = 3
limit_rate = 2m

The -e option of the command line accepts exactly these same directives, and nothing else: -e "tries=3" works, whereas -e "chmod 644" is rejected with the message Invalid --execute command. It is not a shell command, and the confusion is frequent.

Terminal window
wget -nv -O jq https://github.com/jqlang/jq/releases/download/jq-1.7.1/jq-linux-amd64 \
&& echo "5942c9b0934e510ee61eb3e30273f1b3fe2590df93933a93d7c58b81d19c8ff5 jq" | sha256sum -c - \
&& chmod +x jq

This form is also the security good practice: you download, you check the checksum published in the release's sha256sum.txt file, and only then do you make the binary executable. A plain sha256sum jq would not be enough: it prints the checksum without comparing it to anything, and returns code 0 whatever the content. Never pipe a download straight into an interpreter: a compromised server, or a simple cut in the middle of the transfer, would run arbitrary code on your machine before you could even read it.

Common pitfalls

These errors come back with beginners as well as experienced teams. Knowing them saves hours of diagnosis: in most cases, the visible symptom is very far from the real cause.

SymptomCauseFix
.1, .2 files pile upwget does not replace, it numbersAdd -nc (cache) or -N (refresh on the date)
The script carries on despite a 404-q hides the errorsTest $?, or use -nv
rapport-0[1-5].csv returns 404No HTTP globbingShell brace expansion: rapport-0{1..5}.csv
The downloaded file is HTMLThe server returned an error or login page--spider -S to read the code and the headers
The script hangs for several minutes20 retries by default--tries=2 --timeout=10
The mirror fetches the whole siteInfinite depth with -mBound with -l, -np, -Q
The file does not have the right name after a redirectName taken from the original URL--content-disposition or --trust-server-names
The proxy given with -e is ignoredhttp_proxy set for an https:// URLUse -e "https_proxy=..."

Key points

  • wget downloads, curl talks: for a REST API or fine HTTP diagnosis, switch to curl.
  • wget does not overwrite an existing file, it creates fichier.1: use -nc to download nothing again, -N to refresh based on the date.
  • -c resumes an interrupted transfer if the server accepts partial requests, which changes everything on a multi-gigabyte ISO.
  • -q hides errors: in a script, always test the return code (0 success, 4 network failure, 8 server error).
  • Bound your scripts with --tries and --timeout, otherwise wget retries 20 times by default.
  • URL globbing does not exist in HTTP: go through shell brace expansion (fichier-{1..5}.csv), the bracket pattern being specific to curl.
  • -m mirrors without any depth limit: frame it with -np, -l, -Q and --limit-rate so as not to overwhelm the remote server.
  • wget runs nothing after the download: chain in the shell, checking the checksum before making a binary executable, and never pipe a download straight into an interpreter.

Knowledge check

Check that the essentials of this guide are acquired. The questions only cover what has just been explained here.

Knowledge check

Check what you have learned with this interactive quiz

6 questions
6 min
70% to pass

How it works

  • The timer starts when you click Start
  • Multiple choice, true/false and short answer questions
  • You can move between questions
  • Detailed results are shown at the end

Starts the quiz and the timer

Is this project useful to you?

The guides, courses, quizzes and labs stay free. If you use them regularly, you can contribute to their upkeep and development.

Become a supporting member (new tab) One-time support (new tab) Learn more

Follow the project on LinkedIn