A scan you cannot reproduce is a story, not evidence. The moment an Nmap result matters - a firewall change you signed off on, a rogue service you found on a production VLAN, an asset inventory the audit team will lean on - you need it written to a file in a format that survives the terminal scrollback. Saved output lets you diff a segment week over week to catch new or closed ports, feed the results into other tooling, and prove what the network looked like at a specific timestamp. This guide is part of the complete Nmap guide, and it covers the output formats Nmap can write, when each one earns its place, and the short shell one-liners that turn a saved scan into an answer.
Why you save output at all
Running Nmap and reading the screen is fine for a quick look. Everything past that wants a file. Four reasons come up constantly in network work. First, evidence: a timestamped scan file is defensible in a change record or an incident timeline in a way that a screenshot never is. Second, diffing over time: if you scan the same subnet on a schedule and store each run, you can compare them and immediately see what changed - a new listener that appeared overnight, a port that closed after a patch. Third, feeding other tools: XML output is the input format for a whole ecosystem of parsers, importers, and reporting engines. Fourth, asset inventories: grepable output is trivial to fold into a spreadsheet or a CMDB import. Pick the format that matches the downstream job, and Nmap will write it in the same pass as the scan.
The output formats
Nmap writes several formats, each selected by a flag that takes a filename. You can request more than one at a time, and -oA is a shortcut for "give me all the main ones."
The two you will reach for most are -oA basename when you want a full record on disk, and -oG - when you want to pipe grepable output straight into a command. The dash-as-filename trick works for any format flag, so -oX - streams XML to stdout for a downstream parser without ever touching disk.
Normal output, for humans
Normal output is exactly what Nmap prints to the terminal, and it is what most engineers read. Here is a version-detection scan of localhost on a Debian VM, saved with -oA scandemo (which produced scandemo.nmap, scandemo.gnmap, and scandemo.xml in one shot). This is the .nmap file:
# Nmap 7.95 scan initiated Sun Jul 5 15:05:24 2026 as: nmap -sV -p21,22,80 -oA scandemo 127.0.0.1
Nmap scan report for localhost (127.0.0.1)
Host is up (0.000090s latency).
PORT STATE SERVICE VERSION
21/tcp open ftp vsftpd 3.0.5
22/tcp open ssh OpenSSH 10.0p2 Debian 7+deb13u4 (protocol 2.0)
80/tcp open http nginx
Service Info: OSs: Unix, Linux; CPE: cpe:/o:linux:linux_kernel
Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
# Nmap done at Sun Jul 5 15:05:30 2026 -- 1 IP address (1 host up) scanned in 6.84 secondsThis reads cleanly: three open ports, each with a service and a version string that -sV probed out (that is Nmap version detection at work). It is the format to paste into a change ticket or an incident note, because a human can read it without any tooling. What it is not good for is machine parsing - the layout is aligned for eyes, not for awk.
Grepable output, for pipelines
Grepable output collapses each host onto a single line, which is precisely what makes it easy to slice with standard Unix tools. Here is the scandemo.gnmap file from the same run:
# Nmap 7.95 scan initiated Sun Jul 5 15:05:24 2026 as: nmap -sV -p21,22,80 -oA scandemo 127.0.0.1
Host: 127.0.0.1 (localhost) Status: Up
Host: 127.0.0.1 (localhost) Ports: 21/open/tcp//ftp//vsftpd 3.0.5/, 22/open/tcp//ssh//OpenSSH 10.0p2 Debian 7+deb13u4 (protocol 2.0)/, 80/open/tcp//http//nginx/
# Nmap done at Sun Jul 5 15:05:30 2026 -- 1 IP address (1 host up) scanned in 6.84 secondsNotice the single Host: ... Ports: line packs every port for that host into one field, comma-separated, each in a port/state/proto//service//version/ shape. That regular structure is the whole appeal: because it is one line per host, you can grep for a state or awk for a field without wrestling with a multi-line report. From the CML lab, the shorter two-port form shows the same pattern across two hosts at once:
$ nmap -oG - -p 22,80 10.10.10.1 10.10.10.21
# Nmap 7.95 scan initiated Sun Jun 28 07:12:51 2026 as: nmap -oG - -p 22,80 10.10.10.1 10.10.10.21
Host: 10.10.10.1 () Status: Up
Host: 10.10.10.1 () Ports: 22/open/tcp//ssh///, 80/closed/tcp//http///
Host: 10.10.10.21 () Status: Up
Host: 10.10.10.21 () Ports: 22/closed/tcp//ssh///, 80/open/tcp//http///
# Nmap done at Sun Jun 28 07:12:51 2026 -- 2 IP addresses (2 hosts up) scanned in 0.24 secondsHere -oG - streamed straight to stdout. The router (10.10.10.1) has SSH open and HTTP closed (it answered with a RST, not a filter), and the web server (10.10.10.21) is the mirror image. One line per host, ready to pipe.
XML output, for tools
XML is the format you feed to software. It carries everything the scan learned in a strict, parseable structure, and it is the input for ndiff, for importers, and for reporting engines. Here is the ports section of scandemo.xml from the same localhost scan, trimmed to the structure that matters:
<ports><port protocol="tcp" portid="21"><state state="open" reason="syn-ack" reason_ttl="64"/><service name="ftp" product="vsftpd" version="3.0.5" ostype="Unix" method="probed" conf="10"><cpe>cpe:/a:vsftpd:vsftpd:3.0.5</cpe></service></port>
<port protocol="tcp" portid="22"><state state="open" reason="syn-ack" reason_ttl="64"/><service name="ssh" product="OpenSSH" version="10.0p2 Debian 7+deb13u4" extrainfo="protocol 2.0" ostype="Linux" method="probed" conf="10"><cpe>cpe:/a:openbsd:openssh:10.0p2</cpe><cpe>cpe:/o:linux:linux_kernel</cpe></service></port>
<port protocol="tcp" portid="80"><state state="open" reason="syn-ack" reason_ttl="64"/><service name="http" product="nginx" method="probed" conf="10"><cpe>cpe:/a:igor_sysoev:nginx</cpe></service></port>
</ports>Every fact from the normal report is here as an attribute a parser can pull without guessing: each <port> has a <state> element (with the reason and TTL) and a <service> element carrying the product, version, and one or more <cpe> entries. Those CPE strings (cpe:/a:vsftpd:vsftpd:3.0.5, cpe:/a:openbsd:openssh:10.0p2) are the standardized identifiers that vulnerability tooling matches against a CVE database, which is why XML is the format to save when the scan feeds anything automated. You would never read this by eye, and you would never grep it reliably - you hand it to a parser.
Parsing the saved output
The reason grepable output exists is to make quick answers a one-liner. These operate on the saved files above and are standard, everyday usage. To list every open service across a saved .gnmap, split on the comma-separated port field and keep the ones marked open:
$ grep -oE '[0-9]+/open/[^,]+' scandemo.gnmap
21/open/tcp//ftp//vsftpd 3.0.5/
22/open/tcp//ssh//OpenSSH 10.0p2 Debian 7+deb13u4 (protocol 2.0)/
80/open/tcp//http//nginx/To pull a clean host-plus-open-port inventory across a multi-host grepable file, a short awk walks each host line and prints the port numbers that are open:
$ awk '/Ports:/{ip=$2; n=split($0,p,", "); for(i=1;i<=n;i++) if(p[i]~/\/open\//){split(p[i],f,"/"); print ip, f[1]}}' scan.gnmapThat is the payoff of one-line-per-host: no XML library, no scripting language, just the tools already on every box. For anything more structured than a grep - correlating services to CVEs, building a report, diffing two runs - switch to XML and let a real parser do it.
Verbosity, progress, and diffing over time
A few flags shape what lands in the output and how much you see while a long scan runs. -v (and -vv for more) raises verbosity so Nmap reports open ports as it finds them rather than only at the end. --stats-every 10s prints a progress line on a fixed interval, which is a lifesaver on a big sweep so you know it has not hung. --open trims the report to only open ports, cutting the closed and filtered noise when you only care about what is listening. And --reason adds the why behind each state (the syn-ack, reset, and no-response values you saw earlier), which is worth saving whenever the state of a port might be questioned later.
The real long-term value of saved output is diffing. Nmap ships ndiff, which takes two XML scan files and reports exactly what changed between them - hosts that appeared or vanished, ports that opened or closed, services that changed version. Scan a subnet on a schedule, save each run with -oX, and ndiff yesterday.xml today.xml surfaces the new listener that showed up overnight without you rereading a single line. This is how you turn Nmap from a point-in-time tool into a change-detection one. If you would rather grow one running record than keep dated files, --append-output tells Nmap to append to an existing output file instead of overwriting it, though for diffing you generally want discrete per-run files that ndiff can compare cleanly.
Choosing a format in practice
The decision is quick once the downstream job is clear. Reading it yourself or pasting into a ticket? -oN. Grepping or awking a fast inventory? -oG, or -oG - to skip the temp file. Feeding a parser, matching CPEs to CVEs, or diffing with ndiff? -oX. Not sure which you will need later, or building an audit trail? -oA and keep all three. The runtime cost of writing extra formats is effectively zero - the scan does the work, and serializing the results is trivial - so on any scan that matters, defaulting to -oA costs nothing and saves you re-running later. How fast the scan itself finishes is a separate question covered in Nmap timing and performance.
Key takeaways
Save your scans, because a result you cannot reproduce or diff is not evidence. Normal output (-oN) is for humans, grepable output (-oG) collapses one line per host so grep and awk can slice it in a pipeline, and XML (-oX) carries the full structured detail - states, services, and CPE identifiers - that parsers, importers, and ndiff depend on. When in doubt, -oA basename writes all three at once for nothing extra, and the dash-as-filename trick (-oG -) streams a format straight into a pipe. Layer on -v, --stats-every, --open, and --reason to control what you capture and how much you see mid-scan, then use ndiff across dated XML files to catch new or closed ports over time and --append-output when you want one growing record. Pair saved output with solid version detection and you have a repeatable, auditable view of what is really running on your network. For how this fits with discovery, scanning, timing, and the rest of the toolkit, work through the complete Nmap guide.