diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml
index 68eb2bd..4bc5d01 100644
--- a/.github/workflows/rust.yml
+++ b/.github/workflows/rust.yml
@@ -89,6 +89,37 @@ jobs:
           name: 'Cargo.toml'
           path: 'Cargo.toml'
 
+  build:
+    if: github.event_name == 'push' || (github.base_ref == 'main' && github.event.pull_request.merged == true)
+    strategy:
+      matrix:
+        platform: [macos-latest, windows-latest]
+    runs-on: ${{ matrix.platform }}
+    needs: [tag]
+    steps:
+      - name: Checkout
+        uses: actions/checkout@v3.5.2
+      - name: Download Build Artifacts
+        uses: actions/download-artifact@v3.0.2
+        with:
+          name: 'Cargo.toml'
+      - name: Build
+        shell: bash
+        run: |
+          RAW_BINARY_NAME=fcidr
+          BINARY_NAME=${RAW_BINARY_NAME}
+          if [[ ${{ startsWith(matrix.platform, 'windows') }} == true ]]
+          then
+            BINARY_NAME=${BINARY_NAME}.exe
+          fi
+          cargo build --release --verbose
+          cp target/release/${BINARY_NAME} ./
+          tar czf ${RAW_BINARY_NAME}-${{ runner.os }}-${{ runner.arch }}.tar.gz ${BINARY_NAME}
+      - name: Upload Build Artifact
+        uses: actions/upload-artifact@v3.1.2
+        with:
+          path: '*.tar.gz'
+
   publish:
     if: github.event_name == 'push' || (github.base_ref == 'main' && github.event.pull_request.merged == true)
     runs-on: ubuntu-latest
@@ -108,11 +139,12 @@ jobs:
   release:
     if: github.event_name == 'push' || (github.base_ref == 'main' && github.event.pull_request.merged == true)
     runs-on: ubuntu-latest
-    needs: [tag]
+    needs: [tag, build]
     steps:
       - name: Download Build Artifacts
         uses: actions/download-artifact@v3.0.2
       - name: Release
         uses: softprops/action-gh-release@v0.1.15
         with:
+          files: 'artifact/*.tar.gz'
           tag_name: ${{ needs.tag.outputs.version }}
diff --git a/Cargo.toml b/Cargo.toml
index 7c13068..c2ca141 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -4,23 +4,28 @@ version = "0.0.0"
 authors = ["Nicholas Omer Chiasson <nicholasomerchiasson@gmail.com>"]
 edition = "2021"
 license = "MIT"
-description = """
-Fragmented Classless Inter-Domain Routing (FCIDR)
-
-A library exposing a data structure to represent a set of CIDR ranges and
-easily manipulate its entries using set-like operations.
-"""
+description = """Fragmented Classless Inter-Domain Routing (FCIDR)"""
 readme = "README.md"
 homepage = "https://github.com/nicholaschiasson/fcidr"
 repository = "https://github.com/nicholaschiasson/fcidr"
-keywords = ["network", "ip", "ipv4", "cidr"]
-categories = ["data-structures", "network-programming"]
+keywords = ["network", "ip", "ipv4", "cidr", "cli"]
+categories = ["command-line-utilities", "data-structures", "network-programming"]
+rust-version = "1.70.0"
+
+[lib]
+name = "fcidr"
+path = "src/lib.rs"
+
+[[bin]]
+name = "fcidr"
+path = "src/main.rs"
 
 [badges]
 github = { repository = "nicholaschiasson/fcidr" }
 maintenance = { status = "passively-maintained" }
 
 [dependencies]
+clap = { version = "4.3", features = ["derive"] }
 serde = { version = "1.0", optional = true }
 
 [dev-dependencies]
diff --git a/README.md b/README.md
index 4c50998..59ea015 100644
--- a/README.md
+++ b/README.md
@@ -4,8 +4,8 @@
 
 Fragmented Classless Inter-Domain Routing (FCIDR)
 
-A library exposing a data structure to represent a set of CIDR ranges and
-easily manipulate its entries using set-like operations.
+A library exposing a data structure to represent a set of CIDR ranges as well
+as an interface to compute set operations over CIDRs.
 
 This data structure can be applied, for example, in configuring firewalls that
 *implicitly deny* (AWS Security Groups) using a rule set that explicitly
@@ -13,3 +13,80 @@ expresses rules for both allow and deny.
 
 > **Note**
 > Currently, only IPv4 is supported. IPv6 support is tracked by [#6](https://github.com/nicholaschiasson/fcidr/issues/6).
+
+## CLI
+
+This project also publishes a binary application for use on the command line to
+support composing chains of set operations on CIDRs by reading from standard
+input.
+
+### Installation
+
+For now, crates.io is the only place this is being distributed.
+
+```
+cargo install fcidr
+```
+
+### Usage
+
+```
+Fragmented Classless Inter-Domain Routing (FCIDR)
+
+Usage: fcidr [CIDR] <COMMAND>
+
+Commands:
+  complement  Compute the complement of the input CIDR(s)
+  difference  Compute the set difference between the input CIDR(s) and another CIDR [aliases: exclude, minus]
+  union       Compute the set union of the input CIDR(s) and another CIDR [aliases: include, plus]
+  help        Print this message or the help of the given subcommand(s)
+
+Arguments:
+  [CIDR]  The input CIDR range and first operand to the computation. If omitted, input is taken from stdin. In this way, multiple computations can be chained together
+
+Options:
+  -h, --help     Print help
+  -V, --version  Print version
+```
+
+### Example
+
+```
+fcidr 10.0.0.0/8 difference 10.0.64.0/20 | fcidr difference 10.0.82.0/24 | fcidr union 10.0.82.74/31
+10.0.0.0/18
+10.0.80.0/23
+10.0.82.74/31
+10.0.83.0/24
+10.0.84.0/22
+10.0.88.0/21
+10.0.96.0/19
+10.0.128.0/17
+10.1.0.0/16
+10.2.0.0/15
+10.4.0.0/14
+10.8.0.0/13
+10.16.0.0/12
+10.32.0.0/11
+10.64.0.0/10
+10.128.0.0/9
+```
+
+```
+fcidr 10.0.0.0/8 difference 10.0.64.0/20 | fcidr difference 10.0.82.0/24 | fcidr union 10.0.82.74/31 | fcidr complement
+0.0.0.0/5
+8.0.0.0/7
+10.0.64.0/20
+10.0.82.0/26
+10.0.82.64/29
+10.0.82.72/31
+10.0.82.76/30
+10.0.82.80/28
+10.0.82.96/27
+10.0.82.128/25
+11.0.0.0/8
+12.0.0.0/6
+16.0.0.0/4
+32.0.0.0/3
+64.0.0.0/2
+128.0.0.0/1
+```
diff --git a/src/fcidr.rs b/src/fcidr.rs
index 23823fb..1792a7e 100644
--- a/src/fcidr.rs
+++ b/src/fcidr.rs
@@ -248,7 +248,11 @@ impl Iterator for FcidrIntoIterator {
 
 //     #[test]
 //     fn it_works() {
-//         // let mut fcidr = Fcidr::default();
+//         let mut fcidr = Fcidr::default();
+//         fcidr.union("10.0.0.0/8".parse().unwrap());
+//         fcidr.union("10.0.128.0/24".parse().unwrap());
+//         fcidr.difference("10.0.80.0/20".parse().unwrap());
+//         fcidr.union("10.0.82.0/24".parse().unwrap());
 //         // fcidr.union("10.0.0.0/24".parse().unwrap());
 //         // fcidr.union("10.0.128.0/25".parse().unwrap());
 //         // fcidr.union("11.0.0.0/8".parse().unwrap());
@@ -262,9 +266,9 @@ impl Iterator for FcidrIntoIterator {
 //         // fcidr.union("0.0.0.0/0".parse().unwrap());
 //         // fcidr.difference("10.0.0.1/32".parse().unwrap());
 //         // println!("{:?}", fcidr.iter().collect::<Vec<_>>());
-//         // for cidr in &fcidr {
-//         //     println!("{cidr}");
-//         // }
-//         // println!("{fcidr:?}");
+//         for cidr in &fcidr {
+//             println!("{cidr}");
+//         }
+//         println!("{fcidr:?}");
 //     }
 // }
diff --git a/src/main.rs b/src/main.rs
new file mode 100644
index 0000000..762ee8f
--- /dev/null
+++ b/src/main.rs
@@ -0,0 +1,71 @@
+use std::{
+    error::Error,
+    io::{stdin, IsTerminal},
+};
+
+use clap::{CommandFactory, Parser, Subcommand};
+use fcidr::{Cidr, Fcidr};
+
+#[derive(Debug, Parser)]
+#[command(about, author, version, long_about = None)]
+struct Cli {
+    /// The input CIDR range and first operand to the computation. If omitted,
+    /// input is taken from stdin. In this way, multiple computations can be
+    /// chained together.
+    cidr: Option<Cidr>,
+    #[command(subcommand)]
+    command: FcidrCommand,
+}
+
+#[derive(Debug, Subcommand)]
+enum FcidrCommand {
+    /// Compute the complement of the input CIDR(s)
+    Complement,
+    /// Compute the set difference between the input CIDR(s) and another CIDR
+    #[command(visible_alias = "exclude", visible_alias = "minus")]
+    Difference {
+        /// The second CIDR range operand for the difference function
+        cidr: Cidr,
+    },
+    #[command(visible_alias = "include", visible_alias = "plus")]
+    /// Compute the set union of the input CIDR(s) and another CIDR
+    Union {
+        /// The second CIDR range operand for the union function
+        cidr: Cidr,
+    },
+}
+
+fn main() -> Result<(), Box<dyn Error>> {
+    let cli = Cli::parse();
+
+    let mut fcidr: Fcidr = if let Some(cidr) = cli.cidr {
+        Fcidr::new(cidr)
+    } else {
+        if stdin().is_terminal() {
+            Cli::command().print_help().unwrap();
+            ::std::process::exit(2);
+        }
+        stdin().lines().fold(
+            Ok(Fcidr::default()),
+            |fcidr: Result<Fcidr, Box<dyn Error>>, l| {
+                if let Ok(mut fcidr) = fcidr {
+                    fcidr.union(l?.parse()?);
+                    return Ok(fcidr);
+                }
+                fcidr
+            },
+        )?
+    };
+
+    match cli.command {
+        FcidrCommand::Complement => fcidr.complement(),
+        FcidrCommand::Difference { cidr } => fcidr.difference(cidr),
+        FcidrCommand::Union { cidr } => fcidr.union(cidr),
+    };
+
+    for cidr in fcidr {
+        println!("{cidr}");
+    }
+
+    Ok(())
+}