Repository: onecodex/finch-rs (language: python)

Issue:
split finch-rs into library and binary
Libraries [should not have a cargo.lock whereas binaries should](https://doc.rust-lang.org/cargo/faq.html#why-do-binaries-have-cargolock-in-version-control-but-not-libraries). The fact that finch-rs doesn't have a lockfile means that the `finch-rs` binary can't easily continue supporting a particular version of Rust without constantly running CI to verify that it compiles. A (possibly transitive) dependency could be updated at any time, breaking the build (this happened when I was working on #46). A lockfile prevents the build breaking at any time, but is ignored by libraries. The library version of finch-rs would need a minimum rust version of `stable`, since some of its dependencies require stable (for example [`object`](https://crates.io/crates/object)).


Discussion / hints:

Fix patch:
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 40aa9a6..243505c 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -34,6 +34,7 @@ jobs:
       - name: add library to venv
         run: |
           . venv/bin/activate
+          cd lib
           maturin develop --cargo-extra-args="--features=python"
 #      - name: Run the python tests
 #        run: |
@@ -42,6 +43,7 @@ jobs:
       - name: compile taxonomy with python bindings
         run: |
           . venv/bin/activate
+          cd lib
           maturin build --cargo-extra-args="--features=python" --release --strip --manylinux=off
 
   tests:
diff --git a/.gitignore b/.gitignore
index 5b2da7f..e15a567 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,5 +1,4 @@
 target
-Cargo.lock
 .DS_Store
 
 *.swp
diff --git a/Cargo.lock b/Cargo.lock
new file mode 100644
index 0000000..2ae55a4
--- /dev/null
+++ b/Cargo.lock
@@ -0,0 +1,1114 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+[[package]]
+name = "adler"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ee2a4ec343196209d6594e19543ae87a39f96d5534d7174822a3ad825dd6ed7e"
+
+[[package]]
+name = "aho-corasick"
+version = "0.7.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7404febffaa47dac81aa44dba71523c9d069b1bdc50a77db41195149e17f68e5"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "ansi_term"
+version = "0.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ee49baf6cb617b853aa8d93bf420db2383fab46d314482ca2803b40d5fde979b"
+dependencies = [
+ "winapi",
+]
+
+[[package]]
+name = "anyhow"
+version = "1.0.38"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "afddf7f520a80dbf76e6f50a35bca42a2331ef227a28b3b6dc5c2e2338d114b1"
+
+[[package]]
+name = "assert_cmd"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3dc1679af9a1ab4bea16f228b05d18f8363f8327b1fa8db00d2760cfafc6b61e"
+dependencies = [
+ "doc-comment",
+ "predicates",
+ "predicates-core",
+ "predicates-tree",
+ "wait-timeout",
+]
+
+[[package]]
+name = "atty"
+version = "0.2.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8"
+dependencies = [
+ "hermit-abi",
+ "libc",
+ "winapi",
+]
+
+[[package]]
+name = "autocfg"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a"
+
+[[package]]
+name = "bincode"
+version = "1.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f30d3a39baa26f9651f17b375061f3233dde33424a8b72b0dbe93a68a0bc896d"
+dependencies = [
+ "byteorder",
+ "serde",
+]
+
+[[package]]
+name = "bit-set"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6e11e16035ea35e4e5997b393eacbf6f63983188f7a2ad25bfb13465f5ad59de"
+dependencies = [
+ "bit-vec",
+]
+
+[[package]]
+name = "bit-vec"
+version = "0.6.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb"
+
+[[package]]
+name = "bitflags"
+version = "1.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693"
+
+[[package]]
+name = "buf_redux"
+version = "0.8.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b953a6887648bb07a535631f2bc00fbdb2a2216f135552cb3f534ed136b9c07f"
+dependencies = [
+ "memchr",
+ "safemem",
+]
+
+[[package]]
+name = "bytecount"
+version = "0.6.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "72feb31ffc86498dacdbd0fcebb56138e7177a8cc5cea4516031d15ae85a742e"
+
+[[package]]
+name = "byteorder"
+version = "1.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ae44d1a3d5a19df61dd0c8beb138458ac2a53a7ac09eba97d55592540004306b"
+
+[[package]]
+name = "bzip2"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "42b7c3cbf0fa9c1b82308d57191728ca0256cb821220f4e2fd410a72ade26e3b"
+dependencies = [
+ "bzip2-sys",
+ "libc",
+]
+
+[[package]]
+name = "bzip2-sys"
+version = "0.1.9+1.0.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ad3b39a260062fca31f7b0b12f207e8f2590a67d32ec7d59c20484b07ea7285e"
+dependencies = [
+ "cc",
+ "libc",
+ "pkg-config",
+]
+
+[[package]]
+name = "capnp"
+version = "0.14.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ee33f13b9419d35f9ae6e0ee2e5ef6bd059a4d0da2fda16a88ddaf57dfe9acca"
+
+[[package]]
+name = "cc"
+version = "1.0.66"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4c0496836a84f8d0495758516b8621a622beb77c0fed418570e50764093ced48"
+
+[[package]]
+name = "cfg-if"
+version = "0.1.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822"
+
+[[package]]
+name = "cfg-if"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
+
+[[package]]
+name = "clap"
+version = "2.33.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "37e58ac78573c40708d45522f0d80fa2f01cc4f9b4e2bf749807255454312002"
+dependencies = [
+ "ansi_term",
+ "atty",
+ "bitflags",
+ "strsim",
+ "textwrap",
+ "unicode-width",
+ "vec_map",
+]
+
+[[package]]
+name = "const_fn"
+version = "0.4.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "28b9d6de7f49e22cf97ad17fc4036ece69300032f45f78f30b4a4482cdc3f4a6"
+
+[[package]]
+name = "crc32fast"
+version = "1.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "81156fece84ab6a9f2afdb109ce3ae577e42b1228441eded99bd77f627953b1a"
+dependencies = [
+ "cfg-if 1.0.0",
+]
+
+[[package]]
+name = "crossbeam-channel"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dca26ee1f8d361640700bde38b2c37d8c22b3ce2d360e1fc1c74ea4b0aa7d775"
+dependencies = [
+ "cfg-if 1.0.0",
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "crossbeam-deque"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "94af6efb46fef72616855b036a624cf27ba656ffc9be1b9a3c931cfc7749a9a9"
+dependencies = [
+ "cfg-if 1.0.0",
+ "crossbeam-epoch",
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "crossbeam-epoch"
+version = "0.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a1aaa739f95311c2c7887a76863f500026092fb1dce0161dab577e559ef3569d"
+dependencies = [
+ "cfg-if 1.0.0",
+ "const_fn",
+ "crossbeam-utils",
+ "lazy_static",
+ "memoffset",
+ "scopeguard",
+]
+
+[[package]]
+name = "crossbeam-utils"
+version = "0.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "02d96d1e189ef58269ebe5b97953da3274d83a93af647c2ddd6f9dab28cedb8d"
+dependencies = [
+ "autocfg",
+ "cfg-if 1.0.0",
+ "lazy_static",
+]
+
+[[package]]
+name = "ctor"
+version = "0.1.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "10bcb9d7dcbf7002aaffbb53eac22906b64cdcc127971dcc387d8eb7c95d5560"
+dependencies = [
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "difference"
+version = "2.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "524cbf6897b527295dff137cec09ecf3a05f4fddffd7dfcd1585403449e74198"
+
+[[package]]
+name = "doc-comment"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10"
+
+[[package]]
+name = "either"
+version = "1.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457"
+
+[[package]]
+name = "finch"
+version = "0.4.0"
+dependencies = [
+ "anyhow",
+ "assert_cmd",
+ "clap",
+ "finch_lib",
+ "predicates",
+ "proptest",
+ "serde_json",
+]
+
+[[package]]
+name = "finch_lib"
+version = "0.4.0"
+dependencies = [
+ "bincode",
+ "capnp",
+ "memmap",
+ "murmurhash3",
+ "ndarray",
+ "needletail",
+ "numpy",
+ "predicates",
+ "proptest",
+ "pyo3",
+ "rayon",
+ "serde",
+ "serde_json",
+ "thiserror",
+]
+
+[[package]]
+name = "flate2"
+version = "1.0.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7411863d55df97a419aa64cb4d2f167103ea9d767e2c54a1868b7ac3f6b47129"
+dependencies = [
+ "cfg-if 1.0.0",
+ "crc32fast",
+ "libc",
+ "miniz_oxide",
+]
+
+[[package]]
+name = "float-cmp"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e1267f4ac4f343772758f7b1bdcbe767c218bbab93bb432acbf5162bbf85a6c4"
+dependencies = [
+ "num-traits",
+]
+
+[[package]]
+name = "fnv"
+version = "1.0.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
+
+[[package]]
+name = "getrandom"
+version = "0.1.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce"
+dependencies = [
+ "cfg-if 1.0.0",
+ "libc",
+ "wasi 0.9.0+wasi-snapshot-preview1",
+]
+
+[[package]]
+name = "getrandom"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c9495705279e7140bf035dde1f6e750c162df8b625267cd52cc44e0b156732c8"
+dependencies = [
+ "cfg-if 1.0.0",
+ "libc",
+ "wasi 0.10.1+wasi-snapshot-preview1",
+]
+
+[[package]]
+name = "ghost"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1a5bcf1bbeab73aa4cf2fde60a846858dc036163c7c33bec309f8d17de785479"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "hermit-abi"
+version = "0.1.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "322f4de77956e22ed0e5032c359a0f1273f1f7f0d79bfa3b8ffbc730d7fbcc5c"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "indoc"
+version = "1.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e5a75aeaaef0ce18b58056d306c27b07436fbb34b8816c53094b76dd81803136"
+dependencies = [
+ "unindent",
+]
+
+[[package]]
+name = "instant"
+version = "0.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "61124eeebbd69b8190558df225adf7e4caafce0d743919e5d6b19652314ec5ec"
+dependencies = [
+ "cfg-if 1.0.0",
+]
+
+[[package]]
+name = "inventory"
+version = "0.1.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0f0f7efb804ec95e33db9ad49e4252f049e37e8b0a4652e3cd61f7999f2eff7f"
+dependencies = [
+ "ctor",
+ "ghost",
+ "inventory-impl",
+]
+
+[[package]]
+name = "inventory-impl"
+version = "0.1.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "75c094e94816723ab936484666968f5b58060492e880f3c8d00489a1e244fa51"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "itoa"
+version = "0.4.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dd25036021b0de88a0aff6b850051563c6516d0bf53f8638938edbb9de732736"
+
+[[package]]
+name = "lazy_static"
+version = "1.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
+
+[[package]]
+name = "libc"
+version = "0.2.82"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "89203f3fba0a3795506acaad8ebce3c80c0af93f994d5a1d7a0b1eeb23271929"
+
+[[package]]
+name = "lock_api"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dd96ffd135b2fd7b973ac026d28085defbe8983df057ced3eb4f2130b0831312"
+dependencies = [
+ "scopeguard",
+]
+
+[[package]]
+name = "lzma-sys"
+version = "0.1.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bdb4b7c3eddad11d3af9e86c487607d2d2442d185d848575365c4856ba96d619"
+dependencies = [
+ "cc",
+ "libc",
+ "pkg-config",
+]
+
+[[package]]
+name = "matrixmultiply"
+version = "0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "916806ba0031cd542105d916a97c8572e1fa6dd79c9c51e7eb43a09ec2dd84c1"
+dependencies = [
+ "rawpointer",
+]
+
+[[package]]
+name = "memchr"
+version = "2.3.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0ee1c47aaa256ecabcaea351eae4a9b01ef39ed810004e298d2511ed284b1525"
+
+[[package]]
+name = "memmap"
+version = "0.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6585fd95e7bb50d6cc31e20d4cf9afb4e2ba16c5846fc76793f11218da9c475b"
+dependencies = [
+ "libc",
+ "winapi",
+]
+
+[[package]]
+name = "memoffset"
+version = "0.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "157b4208e3059a8f9e78d559edc658e13df41410cb3ae03979c83130067fdd87"
+dependencies = [
+ "autocfg",
+]
+
+[[package]]
+name = "miniz_oxide"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0f2d26ec3309788e423cfbf68ad1800f061638098d76a83681af979dc4eda19d"
+dependencies = [
+ "adler",
+ "autocfg",
+]
+
+[[package]]
+name = "murmurhash3"
+version = "0.0.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a2983372caf4480544083767bf2d27defafe32af49ab4df3a0b7fc90793a3664"
+
+[[package]]
+name = "ndarray"
+version = "0.14.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6c0d5c9540a691d153064dc47a4db2504587a75eae07bf1d73f7a596ebc73c04"
+dependencies = [
+ "matrixmultiply",
+ "num-complex",
+ "num-integer",
+ "num-traits",
+ "rawpointer",
+]
+
+[[package]]
+name = "needletail"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "786b5d6f108373c38d3f96e3e141d8c07abec03a7442528b73fe6a3c432fef9a"
+dependencies = [
+ "buf_redux",
+ "bytecount",
+ "bzip2",
+ "flate2",
+ "memchr",
+ "xz2",
+]
+
+[[package]]
+name = "normalize-line-endings"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "61807f77802ff30975e01f4f071c8ba10c022052f98b3294119f3e615d13e5be"
+
+[[package]]
+name = "num-complex"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "747d632c0c558b87dbabbe6a82f3b4ae03720d0646ac5b7b4dae89394be5f2c5"
+dependencies = [
+ "num-traits",
+]
+
+[[package]]
+name = "num-integer"
+version = "0.1.44"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d2cc698a63b549a70bc047073d2949cce27cd1c7b0a4a862d08a8031bc2801db"
+dependencies = [
+ "autocfg",
+ "num-traits",
+]
+
+[[package]]
+name = "num-traits"
+version = "0.2.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9a64b1ec5cda2586e284722486d802acf1f7dbdc623e2bfc57e65ca1cd099290"
+dependencies = [
+ "autocfg",
+]
+
+[[package]]
+name = "num_cpus"
+version = "1.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "05499f3756671c15885fee9034446956fff3f243d6077b91e5767df161f766b3"
+dependencies = [
+ "hermit-abi",
+ "libc",
+]
+
+[[package]]
+name = "numpy"
+version = "0.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f65308af05bc6952d28e2d51c730dedf6e16d48699795cb7c33d6a9db59f850"
+dependencies = [
+ "cfg-if 0.1.10",
+ "libc",
+ "ndarray",
+ "num-complex",
+ "num-traits",
+ "pyo3",
+]
+
+[[package]]
+name = "parking_lot"
+version = "0.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6d7744ac029df22dca6284efe4e898991d28e3085c706c972bcd7da4a27a15eb"
+dependencies = [
+ "instant",
+ "lock_api",
+ "parking_lot_core",
+]
+
+[[package]]
+name = "parking_lot_core"
+version = "0.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9ccb628cad4f84851442432c60ad8e1f607e29752d0bf072cbd0baf28aa34272"
+dependencies = [
+ "cfg-if 1.0.0",
+ "instant",
+ "libc",
+ "redox_syscall 0.1.57",
+ "smallvec",
+ "winapi",
+]
+
+[[package]]
+name = "paste"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c5d65c4d95931acda4498f675e332fcbdc9a06705cd07086c510e9b6009cd1c1"
+
+[[package]]
+name = "pkg-config"
+version = "0.3.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3831453b3449ceb48b6d9c7ad7c96d5ea673e9b470a1dc578c2ce6521230884c"
+
+[[package]]
+name = "ppv-lite86"
+version = "0.2.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ac74c624d6b2d21f425f752262f42188365d7b8ff1aff74c82e45136510a4857"
+
+[[package]]
+name = "predicates"
+version = "1.0.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "73dd9b7b200044694dfede9edf907c1ca19630908443e9447e624993700c6932"
+dependencies = [
+ "difference",
+ "float-cmp",
+ "normalize-line-endings",
+ "predicates-core",
+ "regex",
+]
+
+[[package]]
+name = "predicates-core"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fb3dbeaaf793584e29c58c7e3a82bbb3c7c06b63cea68d13b0e3cddc124104dc"
+
+[[package]]
+name = "predicates-tree"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "aee95d988ee893cb35c06b148c80ed2cd52c8eea927f50ba7a0be1a786aeab73"
+dependencies = [
+ "predicates-core",
+ "treeline",
+]
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.24"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e0704ee1a7e00d7bb417d0770ea303c1bccbabf0ef1667dae92b5967f5f8a71"
+dependencies = [
+ "unicode-xid",
+]
+
+[[package]]
+name = "proptest"
+version = "0.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "12e6c80c1139113c28ee4670dc50cc42915228b51f56a9e407f0ec60f966646f"
+dependencies = [
+ "bit-set",
+ "bitflags",
+ "byteorder",
+ "lazy_static",
+ "num-traits",
+ "quick-error",
+ "rand 0.7.3",
+ "rand_chacha 0.2.2",
+ "rand_xorshift",
+ "regex-syntax",
+ "rusty-fork",
+ "tempfile",
+]
+
+[[package]]
+name = "pyo3"
+version = "0.13.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "00ca634cf3acd58a599b535ed6cb188223298977d471d146121792bfa23b754c"
+dependencies = [
+ "cfg-if 1.0.0",
+ "ctor",
+ "indoc",
+ "inventory",
+ "libc",
+ "parking_lot",
+ "paste",
+ "pyo3-macros",
+ "unindent",
+]
+
+[[package]]
+name = "pyo3-macros"
+version = "0.13.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "483ac516dbda6789a5b4be0271e7a31b9ad4ec8c0a5955050e8076f72bdbef8f"
+dependencies = [
+ "pyo3-macros-backend",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "pyo3-macros-backend"
+version = "0.13.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "15230cabcda008f03565ed8bac40f094cbb5ee1b46e6551f1ec3a0e922cf7df9"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "quick-error"
+version = "1.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0"
+
+[[package]]
+name = "quote"
+version = "1.0.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "991431c3519a3f36861882da93630ce66b52918dcf1b8e2fd66b397fc96f28df"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "rand"
+version = "0.7.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03"
+dependencies = [
+ "getrandom 0.1.16",
+ "libc",
+ "rand_chacha 0.2.2",
+ "rand_core 0.5.1",
+ "rand_hc 0.2.0",
+]
+
+[[package]]
+name = "rand"
+version = "0.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "18519b42a40024d661e1714153e9ad0c3de27cd495760ceb09710920f1098b1e"
+dependencies = [
+ "libc",
+ "rand_chacha 0.3.0",
+ "rand_core 0.6.1",
+ "rand_hc 0.3.0",
+]
+
+[[package]]
+name = "rand_chacha"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402"
+dependencies = [
+ "ppv-lite86",
+ "rand_core 0.5.1",
+]
+
+[[package]]
+name = "rand_chacha"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e12735cf05c9e10bf21534da50a147b924d555dc7a547c42e6bb2d5b6017ae0d"
+dependencies = [
+ "ppv-lite86",
+ "rand_core 0.6.1",
+]
+
+[[package]]
+name = "rand_core"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19"
+dependencies = [
+ "getrandom 0.1.16",
+]
+
+[[package]]
+name = "rand_core"
+version = "0.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c026d7df8b298d90ccbbc5190bd04d85e159eaf5576caeacf8741da93ccbd2e5"
+dependencies = [
+ "getrandom 0.2.2",
+]
+
+[[package]]
+name = "rand_hc"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c"
+dependencies = [
+ "rand_core 0.5.1",
+]
+
+[[package]]
+name = "rand_hc"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3190ef7066a446f2e7f42e239d161e905420ccab01eb967c9eb27d21b2322a73"
+dependencies = [
+ "rand_core 0.6.1",
+]
+
+[[package]]
+name = "rand_xorshift"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "77d416b86801d23dde1aa643023b775c3a462efc0ed96443add11546cdf1dca8"
+dependencies = [
+ "rand_core 0.5.1",
+]
+
+[[package]]
+name = "rawpointer"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "60a357793950651c4ed0f3f52338f53b2f809f32d83a07f72909fa13e4c6c1e3"
+
+[[package]]
+name = "rayon"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8b0d8e0819fadc20c74ea8373106ead0600e3a67ef1fe8da56e39b9ae7275674"
+dependencies = [
+ "autocfg",
+ "crossbeam-deque",
+ "either",
+ "rayon-core",
+]
+
+[[package]]
+name = "rayon-core"
+version = "1.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9ab346ac5921dc62ffa9f89b7a773907511cdfa5490c572ae9be1be33e8afa4a"
+dependencies = [
+ "crossbeam-channel",
+ "crossbeam-deque",
+ "crossbeam-utils",
+ "lazy_static",
+ "num_cpus",
+]
+
+[[package]]
+name = "redox_syscall"
+version = "0.1.57"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "41cc0f7e4d5d4544e8861606a285bb08d3e70712ccc7d2b84d7c0ccfaf4b05ce"
+
+[[package]]
+name = "redox_syscall"
+version = "0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "05ec8ca9416c5ea37062b502703cd7fcb207736bc294f6e0cf367ac6fc234570"
+dependencies = [
+ "bitflags",
+]
+
+[[package]]
+name = "regex"
+version = "1.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d9251239e129e16308e70d853559389de218ac275b515068abc96829d05b948a"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-syntax",
+ "thread_local",
+]
+
+[[package]]
+name = "regex-syntax"
+version = "0.6.22"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b5eb417147ba9860a96cfe72a0b93bf88fee1744b5636ec99ab20c1aa9376581"
+
+[[package]]
+name = "remove_dir_all"
+version = "0.5.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7"
+dependencies = [
+ "winapi",
+]
+
+[[package]]
+name = "rusty-fork"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cb3dcc6e454c328bb824492db107ab7c0ae8fcffe4ad210136ef014458c1bc4f"
+dependencies = [
+ "fnv",
+ "quick-error",
+ "tempfile",
+ "wait-timeout",
+]
+
+[[package]]
+name = "ryu"
+version = "1.0.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e"
+
+[[package]]
+name = "safemem"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ef703b7cb59335eae2eb93ceb664c0eb7ea6bf567079d843e09420219668e072"
+
+[[package]]
+name = "scopeguard"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd"
+
+[[package]]
+name = "serde"
+version = "1.0.120"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "166b2349061381baf54a58e4b13c89369feb0ef2eaa57198899e2312aac30aab"
+dependencies = [
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_derive"
+version = "1.0.120"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0ca2a8cb5805ce9e3b95435e3765b7b553cecc762d938d409434338386cb5775"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "serde_json"
+version = "1.0.61"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4fceb2595057b6891a4ee808f70054bd2d12f0e97f1cbb78689b59f676df325a"
+dependencies = [
+ "itoa",
+ "ryu",
+ "serde",
+]
+
+[[package]]
+name = "smallvec"
+version = "1.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fe0f37c9e8f3c5a4a66ad655a93c74daac4ad00c441533bf5c6e7990bb42604e"
+
+[[package]]
+name = "strsim"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a"
+
+[[package]]
+name = "syn"
+version = "1.0.58"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cc60a3d73ea6594cd712d830cc1f0390fd71542d8c8cd24e70cc54cdfd5e05d5"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-xid",
+]
+
+[[package]]
+name = "tempfile"
+version = "3.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dac1c663cfc93810f88aed9b8941d48cabf856a1b111c29a40439018d870eb22"
+dependencies = [
+ "cfg-if 1.0.0",
+ "libc",
+ "rand 0.8.2",
+ "redox_syscall 0.2.4",
+ "remove_dir_all",
+ "winapi",
+]
+
+[[package]]
+name = "textwrap"
+version = "0.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060"
+dependencies = [
+ "unicode-width",
+]
+
+[[package]]
+name = "thiserror"
+version = "1.0.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "76cc616c6abf8c8928e2fdcc0dbfab37175edd8fb49a4641066ad1364fdab146"
+dependencies = [
+ "thiserror-impl",
+]
+
+[[package]]
+name = "thiserror-impl"
+version = "1.0.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9be73a2caec27583d0046ef3796c3794f868a5bc813db689eed00c7631275cd1"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "thread_local"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bb9bc092d0d51e76b2b19d9d85534ffc9ec2db959a2523cdae0697e2972cd447"
+dependencies = [
+ "lazy_static",
+]
+
+[[package]]
+name = "treeline"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a7f741b240f1a48843f9b8e0444fb55fb2a4ff67293b50a9179dfd5ea67f8d41"
+
+[[package]]
+name = "unicode-width"
+version = "0.1.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9337591893a19b88d8d87f2cec1e73fad5cdfd10e5a6f349f498ad6ea2ffb1e3"
+
+[[package]]
+name = "unicode-xid"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f7fe0bb3479651439c9112f72b6c505038574c9fbb575ed1bf3b797fa39dd564"
+
+[[package]]
+name = "unindent"
+version = "0.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f14ee04d9415b52b3aeab06258a3f07093182b88ba0f9b8d203f211a7a7d41c7"
+
+[[package]]
+name = "vec_map"
+version = "0.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191"
+
+[[package]]
+name = "wait-timeout"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9f200f5b12eb75f8c1ed65abd4b2db8a6e1b138a20de009dacee265a2498f3f6"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "wasi"
+version = "0.9.0+wasi-snapshot-preview1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519"
+
+[[package]]
+name = "wasi"
+version = "0.10.1+wasi-snapshot-preview1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "93c6c3420963c5c64bca373b25e77acb562081b9bb4dd5bb864187742186cea9"
+
+[[package]]
+name = "winapi"
+version = "0.3.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
+dependencies = [
+ "winapi-i686-pc-windows-gnu",
+ "winapi-x86_64-pc-windows-gnu",
+]
+
+[[package]]
+name = "winapi-i686-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
+
+[[package]]
+name = "winapi-x86_64-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
+
+[[package]]
+name = "xz2"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c179869f34fc7c01830d3ce7ea2086bc3a07e0d35289b667d0a8bf910258926c"
+dependencies = [
+ "lzma-sys",
+]
diff --git a/Cargo.toml b/Cargo.toml
index bef49f8..aa4214b 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,45 +1,5 @@
-[package]
-name = "finch"
-version = "0.4.0"
-authors = ["Roderick Bovee & One Codex <roderick@onecodex.com>"]
-description = "An implementation of min-wise independent permutation locality sensitive hashing ('MinHashing') for genomic data and command-line utility for manipulation."
-keywords = ["minhash", "bioinformatics", "sketches"]
-categories = ["command-line-utilities", "science"]
-repository = "https://github.com/onecodex/finch-rs"
-license = "MIT"
-readme = "./README.md"
-edition = "2018"
-
-[dependencies]
-bincode = "1.2.0"
-capnp = "0.14"
-clap = "2.33.0"
-memmap = "0.7.0"
-murmurhash3 = "0.0.5"
-ndarray = "0.14"
-needletail = "0.4"
-numpy = { version = "0.13", optional = true }
-pyo3 = { version = "0.13", optional = true }
-rayon = "1.2.0"
-serde = "1.0.101"
-serde_derive = "1.0.101"
-serde_json = "1.0.41"
-thiserror = "1"
-
-[dev-dependencies]
-assert_cmd = "1"
-predicates = "1"
-proptest = "0.10"
-
-[features]
-default = []
-python = ["numpy", "pyo3"]
-
-[lib]
-crate-type=["cdylib", "rlib"]
-name = "finch"
-
-
-[package.metadata.maturin]
-maintainer = "One Codex"
-classifier = ["Intended Audience :: Science/Research", "License :: OSI Approved :: MIT License", "Topic :: Scientific/Engineering :: Bio-Informatics"]
+[workspace]
+members = [
+    "cli",
+    "lib",
+]
diff --git a/README.md b/README.md
index d8846a3..9a1e779 100644
--- a/README.md
+++ b/README.md
@@ -173,6 +173,7 @@ Notes:
 - <sup>1</sup> Please see, however, [this issue tracking interoperability](https://github.com/marbl/Mash/issues/27) and note that other implementations may use a different seed value.
 
 ## Python Support ##
+TODO: update me
 
 Optional Python bindings require a nightly rust compiler, and [maturin](https://github.com/PyO3/maturin). Compile Finch into a python library with:
 ```bash
@@ -185,8 +186,10 @@ docker run --rm -v $(pwd):/io konstin2/maturin:master build --cargo-extra-args="
 ```
 
 Then, e.g. to calculate the similarities between two E. coli:
+
 ```python
-from finch import sketch_file
+from cli import sketch_file
+
 sketch_one = sketch_file('WIS_EcoB_v2.fas')
 sketch_two = sketch_file('WIS_Eco10798_DRAFTv1.fas')
 cont, jacc = sketch_one.compare(sketch_two)
@@ -200,8 +203,8 @@ There is a `finch.capnp` in `src/serialization` file and the output of the MinHa
 Both are generated after installing `capnp` and `cargo install capnpc` with the following command:
 
 ```bash
-capnp compile -orust:src/serialization/ --src-prefix=src/serialization/ src/serialization/finch.capnp
-capnp compile -orust:src/serialization/ --src-prefix=src/serialization/ src/serialization/mash.capnp
+capnp compile -orust:finch-lib/src/serialization/ --src-prefix=finch-lib/src/serialization/ finch-lib/src/serialization/finch.capnp
+capnp compile -orust:finch-lib/src/serialization/ --src-prefix=finch-lib/src/serialization/ finch-lib/src/serialization/mash.capnp
 ```
 
 and then search and replace for the `crate::` path to fix it `crate::serialization::`.
diff --git a/cli/Cargo.toml b/cli/Cargo.toml
new file mode 100644
index 0000000..1609c49
--- /dev/null
+++ b/cli/Cargo.toml
@@ -0,0 +1,24 @@
+[package]
+name = "finch"
+version = "0.4.0"
+authors = ["One Codex <vincent@onecodex.com>"]
+description = "An implementation of min-wise independent permutation locality sensitive hashing ('MinHashing') for genomic data and command-line utility for manipulation."
+keywords = ["minhash", "bioinformatics", "sketches"]
+categories = ["command-line-utilities", "science"]
+repository = "https://github.com/onecodex/finch-rs"
+license = "MIT"
+readme = "../README.md"
+edition = "2018"
+
+# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+
+[dependencies]
+clap = "2.33.0"
+finch_lib = { version = "0.4", path = "../lib" }
+serde_json = "1"
+anyhow = "1"
+
+[dev-dependencies]
+assert_cmd = "1"
+predicates = "1"
+proptest = "0.10"
diff --git a/src/main_parsing.rs b/cli/src/cli.rs
similarity index 56%
rename from src/main_parsing.rs
rename to cli/src/cli.rs
index 17b29dd..4b02eea 100644
--- a/src/main_parsing.rs
+++ b/cli/src/cli.rs
@@ -1,49 +1,124 @@
-use std::mem::{discriminant, size_of};
+use anyhow::{anyhow, bail, Result};
+use clap::{crate_authors, crate_version, App, AppSettings, Arg, ArgMatches, SubCommand};
+use finch::filtering::FilterParams;
+use finch::sketch_schemes::SketchParams;
 use std::str::FromStr;
 
-use clap::{App, Arg, ArgMatches};
+pub fn build_cli() -> App<'static, 'static> {
+    App::new("finch")
+        .version(crate_version!())
+        .author(crate_authors!())
+        .about("Tool for working with genomic MinHash sketches")
+        .setting(AppSettings::VersionlessSubcommands)
+        .setting(AppSettings::ArgRequiredElseHelp)
+        .subcommand(info_command())
+        .subcommand(sketch_command())
+        .subcommand(dist_command())
+        .subcommand(hist_command())
+}
 
-use crate::errors::FinchResult;
-use crate::filtering::FilterParams;
-use crate::serialization::Sketch;
-use crate::sketch_schemes::SketchParams;
-use crate::{bail, format_err};
+fn info_command() -> App<'static, 'static> {
+    let mut info_command = SubCommand::with_name("info")
+        .about("Display basic statistics")
+        .arg(
+            Arg::with_name("INPUT")
+                .help("Return stats on these file(s)")
+                .multiple(true)
+                .required(true),
+        );
+    info_command = add_filter_options(info_command);
+    info_command = add_sketch_options(info_command);
+    info_command
+}
 
-pub fn get_int_arg<T: FromStr>(matches: &ArgMatches, key: &str) -> FinchResult<T> {
-    let display_key = key.replace("_", "-");
-    Ok(matches
-        .value_of(key)
-        .ok_or_else(|| format_err!("Bad {}", display_key))?
-        .parse::<T>()
-        .map_err(|_| {
-            if size_of::<T>() == 1 {
-                format_err!("{} must be a positive integer (<256)", display_key)
-            } else {
-                format_err!("{} must be a positive integer", display_key)
-            }
-        })?)
+fn sketch_command() -> App<'static, 'static> {
+    let mut sketch_command = SubCommand::with_name("sketch")
+        .about("Create sketches from FASTA/Q file(s)")
+        .arg(
+            Arg::with_name("INPUT")
+                .help("The file(s) to sketch")
+                .multiple(true)
+                .required(true),
+        )
+        .arg(
+            Arg::with_name("binary_format")
+                .short("b")
+                .long("finch-binary-format")
+                .help("Outputs sketch to a finch-native binary format"),
+        )
+        .arg(
+            Arg::with_name("mash_binary_format")
+                .short("B")
+                .long("mash-binary-format")
+                .conflicts_with("binary_format")
+                .help("Outputs sketch in a binary format compatible with `mash`"),
+        );
+    sketch_command = add_output_options(sketch_command);
+    sketch_command = add_filter_options(sketch_command);
+    sketch_command = add_sketch_options(sketch_command);
+    sketch_command
 }
 
-pub fn get_float_arg<T: Copy + FromStr + Into<f64>>(
-    matches: &ArgMatches,
-    key: &str,
-    limit: f64,
-) -> FinchResult<T> {
-    let display_key = key.replace("_", "-");
-    Ok(matches
-        .value_of(key)
-        .ok_or_else(|| format_err!("Bad {}", display_key))?
-        .parse::<T>()
-        .map_err(|_| format_err!("{} must be a number", display_key))
-        .and_then(|r| {
-            if 0f64 <= r.into() && r.into() <= limit {
-                return Ok(r);
-            }
-            bail!("{} must be between 0 and {}", display_key, limit)
-        })?)
+fn dist_command() -> App<'static, 'static> {
+    let mut dist_command = SubCommand::with_name("dist")
+        .about("Compute distances between sketches")
+        .arg(
+            Arg::with_name("INPUT")
+                .help("Sketchfile(s) to make comparisons for")
+                .multiple(true)
+                .required(true),
+        )
+        .arg(
+            Arg::with_name("pairwise")
+                .short("p")
+                .long("pairwise")
+                .conflicts_with("queries")
+                .help("Calculate distances between all sketches"),
+        )
+        .arg(
+            Arg::with_name("queries")
+                .short("q")
+                .long("queries")
+                .help("All distances are from these sketches (sketches must be in the first file)")
+                .multiple(true)
+                .conflicts_with("pairwise")
+                .takes_value(true),
+        )
+        .arg(
+            Arg::with_name("max_distance")
+                .short("d")
+                .long("max-dist")
+                .help("Only report distances under this threshold")
+                .default_value("1.0")
+                .takes_value(true),
+        )
+        .arg(
+            Arg::with_name("old_dist_mode")
+                .long("old-dist")
+                .help("Calculate distances using the old containment-biased Finch mode"),
+        );
+    dist_command = add_output_options(dist_command);
+    dist_command = add_filter_options(dist_command);
+    dist_command = add_sketch_options(dist_command);
+    dist_command
+}
+
+fn hist_command() -> App<'static, 'static> {
+    let mut hist_command = SubCommand::with_name("hist")
+        .about("Display histograms of kmer abundances")
+        .arg(
+            Arg::with_name("INPUT")
+                .help("Generate histograms from these file(s)")
+                .multiple(true)
+                .required(true),
+        );
+    hist_command = add_output_options(hist_command);
+    hist_command = add_filter_options(hist_command);
+    hist_command = add_sketch_options(hist_command);
+    hist_command
 }
 
-pub fn add_filter_options<'a, 'b>(app: App<'a, 'b>) -> App<'a, 'b> {
+fn add_filter_options<'a, 'b>(app: App<'a, 'b>) -> App<'a, 'b> {
     app.arg(Arg::with_name("no_filter")
          .long("no-filter")
          .conflicts_with("filter")
@@ -72,44 +147,7 @@ pub fn add_filter_options<'a, 'b>(app: App<'a, 'b>) -> App<'a, 'b> {
          .default_value("1"))
 }
 
-pub fn parse_filter_options(matches: &ArgMatches, kmer_length: u8) -> FinchResult<FilterParams> {
-    let filter_on = match (
-        matches.is_present("filter"),
-        matches.is_present("no_filter"),
-    ) {
-        (true, true) => panic!("Can't have both filtering and no filtering!"),
-        (true, false) => Some(true),
-        (false, true) => Some(false),
-        (false, false) => None,
-    };
-
-    let min_abun_filter = if matches.occurrences_of("min_abun_filter") > 0 {
-        Some(get_int_arg::<u32>(matches, "min_abun_filter")?)
-    } else {
-        None
-    };
-
-    let max_abun_filter = if matches.occurrences_of("max_abun_filter") > 0 {
-        Some(get_int_arg::<u32>(matches, "max_abun_filter")?)
-    } else {
-        None
-    };
-
-    let mut err_filter =
-        get_float_arg::<f64>(matches, "err_filter", 100f64 / f64::from(kmer_length))?;
-    err_filter *= f64::from(kmer_length) / 100f64;
-
-    let strand_filter = get_float_arg::<f64>(matches, "strand_filter", 1f64)?;
-
-    Ok(FilterParams {
-        filter_on,
-        abun_filter: (min_abun_filter, max_abun_filter),
-        err_filter,
-        strand_filter,
-    })
-}
-
-pub fn add_sketch_options<'a, 'b>(app: App<'a, 'b>) -> App<'a, 'b> {
+fn add_sketch_options<'a, 'b>(app: App<'a, 'b>) -> App<'a, 'b> {
     // note we're defining groups for the arguments depending on which
     // sketch_type they're used with, but clap doesn't allow us to flag
     // argument conflicts/requirements based off another argument's value
@@ -159,11 +197,88 @@ pub fn add_sketch_options<'a, 'b>(app: App<'a, 'b>) -> App<'a, 'b> {
          .help("Allow sketching files with fewer kmers than `n_hashes` [`sketch-type=mash` only]"))
 }
 
+fn add_output_options<'a, 'b>(app: App<'a, 'b>) -> App<'a, 'b> {
+    app.arg(
+        Arg::with_name("output_file")
+            .short("o")
+            .long("output")
+            .help("Output to this file")
+            .takes_value(true),
+    )
+    .arg(
+        Arg::with_name("std_out")
+            .short("O")
+            .long("std-out")
+            .help("Output to stdout ('print to terminal')")
+            .conflicts_with("output_file"),
+    )
+}
+
+pub fn get_int_arg<T: FromStr>(matches: &ArgMatches, key: &str) -> Result<T> {
+    let display_key = key.replace("_", "-");
+    Ok(matches
+        .value_of(key)
+        .ok_or_else(|| anyhow!("Bad {}", display_key))?
+        .parse::<T>()
+        .map_err(|_| anyhow!("{} must be a positive integer", display_key))?)
+}
+
+pub fn get_float_arg(matches: &ArgMatches, key: &str, limit: f64) -> Result<f64> {
+    let display_key = key.replace("_", "-");
+    Ok(matches
+        .value_of(key)
+        .ok_or_else(|| anyhow!("Bad {}", display_key))?
+        .parse::<f64>()
+        .map_err(|_| anyhow!("{} must be a number", display_key))
+        .and_then(|r| {
+            if 0f64 <= r && r <= limit {
+                return Ok(r);
+            }
+            bail!("{} must be between 0 and {}", display_key, limit)
+        })?)
+}
+
+pub fn parse_filter_options(matches: &ArgMatches, kmer_length: u8) -> Result<FilterParams> {
+    let filter_on = match (
+        matches.is_present("filter"),
+        matches.is_present("no_filter"),
+    ) {
+        (true, true) => panic!("Can't have both filtering and no filtering!"),
+        (true, false) => Some(true),
+        (false, true) => Some(false),
+        (false, false) => None,
+    };
+
+    let min_abun_filter = if matches.occurrences_of("min_abun_filter") > 0 {
+        Some(get_int_arg::<u32>(matches, "min_abun_filter")?)
+    } else {
+        None
+    };
+
+    let max_abun_filter = if matches.occurrences_of("max_abun_filter") > 0 {
+        Some(get_int_arg::<u32>(matches, "max_abun_filter")?)
+    } else {
+        None
+    };
+
+    let mut err_filter = get_float_arg(matches, "err_filter", 100f64 / f64::from(kmer_length))?;
+    err_filter *= f64::from(kmer_length) / 100f64;
+
+    let strand_filter = get_float_arg(matches, "strand_filter", 1f64)?;
+
+    Ok(FilterParams {
+        filter_on,
+        abun_filter: (min_abun_filter, max_abun_filter),
+        err_filter,
+        strand_filter,
+    })
+}
+
 pub fn parse_sketch_options(
     matches: &ArgMatches,
     kmer_length: u8,
     filters_enabled: Option<bool>,
-) -> FinchResult<SketchParams> {
+) -> Result<SketchParams> {
     Ok(match matches.value_of("sketch_type").unwrap_or("mash") {
         "mash" => {
             if matches.occurrences_of("scale") != 0 {
@@ -220,113 +335,6 @@ pub fn parse_sketch_options(
             }
             SketchParams::AllCounts { kmer_length }
         }
-        _ => panic!("A unknown sketch type was selected"),
+        _ => bail!("A unknown sketch type was selected"),
     })
 }
-
-pub fn update_sketch_params(
-    matches: &ArgMatches,
-    sketch_params: &mut SketchParams,
-    sketch: &Sketch,
-    name: &str,
-) -> FinchResult<()> {
-    let new_sketch_params = &sketch.sketch_params;
-
-    // check that the sketching type is the same; we may want to remove
-    // this check at some point?
-    if discriminant(sketch_params) != discriminant(new_sketch_params) {
-        bail!("Sketch types are not the same")
-    }
-
-    // if arguments weren't provided use the ones from the multisketch
-    match sketch_params {
-        SketchParams::Mash {
-            final_size,
-            kmer_length,
-            hash_seed,
-            ..
-        } => {
-            if matches.occurrences_of("n_hashes") == 0 {
-                *final_size = new_sketch_params.expected_size();
-            }
-            if matches.occurrences_of("kmer_length") == 0 {
-                *kmer_length = new_sketch_params.k();
-            } else if *kmer_length != new_sketch_params.k() {
-                bail!(
-                    "Specified kmer length {} does not match {} from sketch {}",
-                    kmer_length,
-                    new_sketch_params.k(),
-                    name
-                );
-            }
-            let (_, _, new_hash_seed, _) = new_sketch_params.hash_info();
-            if matches.occurrences_of("seed") == 0 {
-                *hash_seed = new_hash_seed;
-            } else if *hash_seed != new_hash_seed {
-                bail!(
-                    "Specified hash seed {} does not match {} from sketch {}",
-                    hash_seed,
-                    new_hash_seed,
-                    name
-                );
-            }
-            // TODO: do we need to update any of the other sketch params?
-        }
-        SketchParams::Scaled {
-            kmer_length,
-            hash_seed,
-            scale,
-            ..
-        } => {
-            // TODO: these two are identical to above so some DRY might be good?
-            if matches.occurrences_of("kmer_length") == 0 {
-                *kmer_length = new_sketch_params.k();
-            } else if *kmer_length != new_sketch_params.k() {
-                bail!(
-                    "Specified kmer length {} does not match {} from sketch {}",
-                    kmer_length,
-                    new_sketch_params.k(),
-                    name
-                );
-            }
-            let (_, _, new_hash_seed, new_scale) = new_sketch_params.hash_info();
-            if matches.occurrences_of("seed") == 0 {
-                *hash_seed = new_hash_seed;
-            } else if *hash_seed != new_hash_seed {
-                bail!(
-                    "Specified hash seed {} does not match {} from sketch {}",
-                    hash_seed,
-                    new_hash_seed,
-                    name
-                );
-            }
-
-            if let Some(new_scale_num) = new_scale {
-                if matches.occurrences_of("scale") == 0 {
-                    *scale = new_scale_num;
-                } else if (*scale - new_scale_num).abs() < std::f64::EPSILON {
-                    // TODO: maybe this should have a slightly larger delta?
-                    bail!(
-                        "Specified scale {} does not match {} from sketch {}",
-                        scale,
-                        new_scale_num,
-                        name
-                    );
-                }
-            }
-        }
-        SketchParams::AllCounts { kmer_length } => {
-            if matches.occurrences_of("kmer_length") == 0 {
-                *kmer_length = new_sketch_params.k();
-            } else if *kmer_length != new_sketch_params.k() {
-                bail!(
-                    "Specified kmer length {} does not match {} from sketch {}",
-                    kmer_length,
-                    new_sketch_params.k(),
-                    name
-                );
-            }
-        }
-    }
-    Ok(())
-}
diff --git a/cli/src/main.rs b/cli/src/main.rs
new file mode 100644
index 0000000..e25010b
--- /dev/null
+++ b/cli/src/main.rs
@@ -0,0 +1,441 @@
+use std::collections::{HashMap, HashSet};
+use std::fs::File;
+use std::io::{stdout, Write};
+
+use anyhow::{anyhow, bail, Context, Result};
+use clap::ArgMatches;
+
+use crate::cli::{get_float_arg, get_int_arg, parse_filter_options, parse_sketch_options};
+use finch::distance::distance;
+use finch::serialization::{
+    write_finch_file, write_mash_file, MultiSketch, Sketch, SketchDistance, FINCH_BIN_EXT,
+    FINCH_EXT, MASH_EXT,
+};
+use finch::sketch_schemes::SketchParams;
+use finch::statistics::{cardinality, hist};
+use finch::{format_err, open_sketch_file, sketch_files};
+use std::mem::discriminant;
+
+mod cli;
+
+fn output_to<F>(output_fn: F, output: Option<&str>, extension: &str) -> Result<()>
+where
+    F: Fn(&mut dyn Write) -> Result<()>,
+{
+    match output {
+        None => {
+            let mut out = stdout();
+            output_fn(&mut out)?;
+        }
+        Some(o) => {
+            // if the filename doesn't have the right extension
+            // add it on
+            let filename = String::from(o);
+            let out_filename = if filename.ends_with(extension) {
+                filename
+            } else {
+                filename + extension
+            };
+
+            let mut out = File::create(&out_filename)
+                .context(format!("unable to create '{}'", out_filename))?;
+            output_fn(&mut out)?;
+        }
+    };
+    Ok(())
+}
+
+fn run() -> Result<()> {
+    let matches = cli::build_cli().get_matches();
+
+    match matches.subcommand() {
+        ("sketch", Some(matches)) => {
+            let file_ext = if matches.is_present("binary_format") {
+                FINCH_BIN_EXT
+            } else if matches.is_present("mash_binary_format") {
+                MASH_EXT
+            } else {
+                FINCH_EXT
+            };
+
+            if matches.is_present("output_file") || matches.is_present("std_out") {
+                let sketches = parse_mash_files(matches)?;
+                let output = matches.value_of("output_file");
+
+                output_to(
+                    |writer| {
+                        if file_ext == FINCH_BIN_EXT {
+                            write_finch_file(writer, &sketches)?;
+                        } else if file_ext == MASH_EXT {
+                            write_mash_file(writer, &sketches)?;
+                        } else {
+                            let multisketch = MultiSketch::from_sketches(&sketches)?;
+                            serde_json::to_writer(writer, &multisketch)?;
+                        }
+                        Ok(())
+                    },
+                    output,
+                    &file_ext,
+                )?;
+            } else {
+                // special case for "sketching in place"
+                generate_sketch_files(matches, file_ext)?;
+            }
+        }
+        ("dist", Some(matches)) => {
+            let old_mode = matches.is_present("old_dist_mode");
+
+            let max_dist = get_float_arg(matches, "max_distance", 1f64)?;
+            let all_sketches = parse_mash_files(matches)?;
+
+            let mut query_sketches = Vec::new();
+            if matches.is_present("pairwise") {
+                for sketch in &all_sketches {
+                    query_sketches.push(sketch);
+                }
+            } else if matches.is_present("queries") {
+                let query_names: HashSet<String> = matches
+                    .values_of("queries")
+                    .unwrap() // we already know it's present
+                    .map(|s| s.to_string())
+                    .collect();
+
+                for sketch in &all_sketches {
+                    if query_names.contains(&sketch.name) {
+                        query_sketches.push(&sketch);
+                    }
+                }
+            } else {
+                if all_sketches.is_empty() {
+                    bail!("No sketches present!");
+                }
+                query_sketches.push(all_sketches.first().unwrap());
+            }
+
+            let distances =
+                calc_sketch_distances(&query_sketches, &all_sketches, old_mode, max_dist);
+
+            output_to(
+                |writer| {
+                    serde_json::to_writer(writer, &distances)
+                        .map_err(|_| anyhow!("Could not serialize JSON to file"))?;
+                    Ok(())
+                },
+                matches.value_of("output_file"),
+                ".json",
+            )?;
+        }
+        ("hist", Some(matches)) => {
+            let mut hist_map: HashMap<String, Vec<u64>> = HashMap::new();
+            let multisketch = parse_mash_files(matches)?;
+
+            for sketch in multisketch {
+                hist_map.insert(sketch.name.to_string(), hist(&sketch.hashes));
+            }
+
+            output_to(
+                |writer| {
+                    serde_json::to_writer(writer, &hist_map)
+                        .map_err(|_| anyhow!("Could not serialize JSON to file"))?;
+                    Ok(())
+                },
+                matches.value_of("output_file"),
+                ".json",
+            )?;
+        }
+        ("info", Some(matches)) => {
+            // TODO: this should probably output JSON
+            let multisketch = parse_mash_files(matches)?;
+
+            for sketch in multisketch {
+                print!("{}", &sketch.name);
+                println!(" (from {}bp)", sketch.seq_length);
+                let kmers = &sketch.hashes;
+                if let Ok(c) = cardinality(kmers) {
+                    println!("  Estimated # of Unique Kmers: {}", c);
+                }
+
+                let histogram = hist(kmers);
+                let mean = histogram
+                    .iter()
+                    .enumerate()
+                    .map(|(i, v)| ((i as f32 + 1f32) * *v as f32, *v as f32))
+                    .fold((0f32, 0f32), |e, s| (e.0 + s.0, e.1 + s.1));
+                println!("  Estimated Average Depth: {}x", mean.0 / mean.1);
+
+                let mut total_gc: u64 = 0;
+                for kmer in kmers {
+                    total_gc += kmer
+                        .kmer
+                        .iter()
+                        .map(|b| match *b {
+                            b'G' | b'g' | b'C' | b'c' => u64::from(kmer.count),
+                            _ => 0,
+                        })
+                        .sum::<u64>();
+                }
+                let total_bases = if kmers.is_empty() {
+                    0f32
+                } else {
+                    mean.0 * kmers[0].kmer.len() as f32
+                };
+                println!(
+                    "  Estimated % GC: {}%",
+                    100f32 * total_gc as f32 / total_bases
+                );
+            }
+        }
+        other => bail!("Unknown subcommand: {:?}", other.0),
+    };
+
+    Ok(())
+}
+
+fn main() {
+    if let Err(err) = run() {
+        eprintln!("Error: {:?}", err);
+        std::process::exit(1);
+    }
+}
+
+fn generate_sketch_files(matches: &ArgMatches, file_ext: &str) -> Result<()> {
+    let filenames: Vec<_> = matches
+        .values_of("INPUT")
+        .ok_or_else(|| format_err!("Bad INPUT"))?
+        .collect();
+
+    let kmer_length: u8 = get_int_arg(matches, "kmer_length")?;
+    let filters = parse_filter_options(matches, kmer_length)?;
+    let sketch_params = parse_sketch_options(matches, kmer_length, filters.filter_on)?;
+
+    for filename in filenames {
+        if filename.ends_with(".json")
+            || filename.ends_with(FINCH_EXT)
+            || filename.ends_with(FINCH_BIN_EXT)
+            || filename.ends_with(MASH_EXT)
+        {
+            bail!("Filename {} is not a sequence file?", filename);
+        }
+
+        let sketches = sketch_files(&[filename], &sketch_params, &filters)?;
+
+        let out_filename = filename.to_string() + file_ext;
+        let mut out = File::create(&out_filename)
+            .map_err(|_| format_err!("Could not open {}", out_filename))?;
+        if matches.is_present("binary_format") {
+            write_finch_file(&mut out, &sketches)?;
+        } else if matches.is_present("mash_binary_format") {
+            write_mash_file(&mut out, &sketches)?;
+        } else {
+            let multisketch: MultiSketch = MultiSketch::from_sketches(&sketches)?;
+            serde_json::to_writer(&mut out, &multisketch)?;
+        }
+    }
+    Ok(())
+}
+
+fn parse_mash_files(matches: &ArgMatches) -> Result<Vec<Sketch>> {
+    let filenames: Vec<_> = matches
+        .values_of("INPUT")
+        .ok_or_else(|| anyhow!("Bad INPUT"))?
+        .collect();
+
+    let mut sketch_filenames = Vec::new();
+    let mut seq_filenames = Vec::new();
+    for filename in filenames {
+        if filename.ends_with(".json")
+            || filename.ends_with(FINCH_EXT)
+            || filename.ends_with(FINCH_BIN_EXT)
+            || filename.ends_with(MASH_EXT)
+        {
+            sketch_filenames.push(filename);
+        } else {
+            seq_filenames.push(filename);
+        }
+    }
+
+    let kmer_length: u8 = get_int_arg(matches, "kmer_length")?;
+    let mut filters = parse_filter_options(matches, kmer_length)?;
+    let mut sketch_params = parse_sketch_options(matches, kmer_length, filters.filter_on)?;
+
+    let mut filename_iter = sketch_filenames.iter();
+    if let Some(first_filename) = filename_iter.next() {
+        let mut sketches = open_sketch_file(first_filename)?;
+
+        update_sketch_params(matches, &mut sketch_params, &sketches[0], first_filename)?;
+        // we also have to handle updating filter options separately because
+        // kmer_length changes how we calculate the `err_filter`
+        if matches.occurrences_of("kmer_length") == 0 {
+            filters = parse_filter_options(matches, sketch_params.k())?;
+        }
+
+        // now do the filtering for the first sketch file
+        if filters.filter_on == Some(true) {
+            for mut sketch in &mut sketches {
+                filters.filter_sketch(&mut sketch);
+            }
+        }
+
+        // and then handle the rest of the sketch files
+        for filename in filename_iter {
+            let extra_sketches = open_sketch_file(filename)?;
+            // check new sketches are compatible with original file
+            for sketch in &extra_sketches {
+                if let Some((name, v1, v2)) =
+                    sketch_params.check_compatibility(&sketch.sketch_params)
+                {
+                    bail!(
+                        "Sketch {} has {} {}, but working value is {}",
+                        sketch.name,
+                        name,
+                        v2,
+                        v1,
+                    );
+                }
+            }
+            sketches.extend(extra_sketches);
+            if filters.filter_on == Some(true) {
+                for mut sketch in &mut sketches {
+                    filters.filter_sketch(&mut sketch);
+                }
+            }
+        }
+
+        // now handle the sequences
+        let extra_sketches = sketch_files(&seq_filenames, &sketch_params, &filters)?;
+        sketches.extend(extra_sketches);
+        Ok(sketches)
+    } else {
+        // now handle the sequences
+        let sketches = sketch_files(&seq_filenames, &sketch_params, &filters)?;
+        Ok(sketches)
+    }
+}
+
+fn calc_sketch_distances(
+    query_sketches: &[&Sketch],
+    ref_sketches: &[Sketch],
+    old_mode: bool,
+    max_distance: f64,
+) -> Vec<SketchDistance> {
+    let mut distances = Vec::new();
+    for ref_sketch in ref_sketches {
+        for query_sketch in query_sketches {
+            if query_sketch == &ref_sketch {
+                continue;
+            }
+            let distance = distance(&query_sketch, &ref_sketch, old_mode).unwrap();
+            if distance.mash_distance <= max_distance {
+                distances.push(distance);
+            }
+        }
+    }
+    distances
+}
+
+pub fn update_sketch_params(
+    matches: &ArgMatches,
+    sketch_params: &mut SketchParams,
+    sketch: &Sketch,
+    name: &str,
+) -> Result<()> {
+    let new_sketch_params = &sketch.sketch_params;
+
+    // check that the sketching type is the same; we may want to remove
+    // this check at some point?
+    if discriminant(sketch_params) != discriminant(new_sketch_params) {
+        bail!("Sketch types are not the same")
+    }
+
+    // if arguments weren't provided use the ones from the multisketch
+    match sketch_params {
+        SketchParams::Mash {
+            final_size,
+            kmer_length,
+            hash_seed,
+            ..
+        } => {
+            if matches.occurrences_of("n_hashes") == 0 {
+                *final_size = new_sketch_params.expected_size();
+            }
+            if matches.occurrences_of("kmer_length") == 0 {
+                *kmer_length = new_sketch_params.k();
+            } else if *kmer_length != new_sketch_params.k() {
+                bail!(
+                    "Specified kmer length {} does not match {} from sketch {}",
+                    kmer_length,
+                    new_sketch_params.k(),
+                    name
+                );
+            }
+            let (_, _, new_hash_seed, _) = new_sketch_params.hash_info();
+            if matches.occurrences_of("seed") == 0 {
+                *hash_seed = new_hash_seed;
+            } else if *hash_seed != new_hash_seed {
+                bail!(
+                    "Specified hash seed {} does not match {} from sketch {}",
+                    hash_seed,
+                    new_hash_seed,
+                    name
+                );
+            }
+            // TODO: do we need to update any of the other sketch params?
+        }
+        SketchParams::Scaled {
+            kmer_length,
+            hash_seed,
+            scale,
+            ..
+        } => {
+            // TODO: these two are identical to above so some DRY might be good?
+            if matches.occurrences_of("kmer_length") == 0 {
+                *kmer_length = new_sketch_params.k();
+            } else if *kmer_length != new_sketch_params.k() {
+                bail!(
+                    "Specified kmer length {} does not match {} from sketch {}",
+                    kmer_length,
+                    new_sketch_params.k(),
+                    name
+                );
+            }
+            let (_, _, new_hash_seed, new_scale) = new_sketch_params.hash_info();
+            if matches.occurrences_of("seed") == 0 {
+                *hash_seed = new_hash_seed;
+            } else if *hash_seed != new_hash_seed {
+                bail!(
+                    "Specified hash seed {} does not match {} from sketch {}",
+                    hash_seed,
+                    new_hash_seed,
+                    name
+                );
+            }
+
+            if let Some(new_scale_num) = new_scale {
+                if matches.occurrences_of("scale") == 0 {
+                    *scale = new_scale_num;
+                } else if (*scale - new_scale_num).abs() < std::f64::EPSILON {
+                    // TODO: maybe this should have a slightly larger delta?
+                    bail!(
+                        "Specified scale {} does not match {} from sketch {}",
+                        scale,
+                        new_scale_num,
+                        name
+                    );
+                }
+            }
+        }
+        SketchParams::AllCounts { kmer_length } => {
+            if matches.occurrences_of("kmer_length") == 0 {
+                *kmer_length = new_sketch_params.k();
+            } else if *kmer_length != new_sketch_params.k() {
+                bail!(
+                    "Specified kmer length {} does not match {} from sketch {}",
+                    kmer_length,
+                    new_sketch_params.k(),
+                    name
+                );
+            }
+        }
+    }
+    Ok(())
+}
diff --git a/lib/Cargo.toml b/lib/Cargo.toml
new file mode 100644
index 0000000..8709efc
--- /dev/null
+++ b/lib/Cargo.toml
@@ -0,0 +1,41 @@
+[package]
+name = "finch_lib"
+version = "0.4.0"
+authors = ["One Codex <vincent@onecodex.com>"]
+description = "An implementation of min-wise independent permutation locality sensitive hashing ('MinHashing') for genomic data and command-line utility for manipulation."
+keywords = ["minhash", "bioinformatics", "sketches"]
+categories = ["science"]
+repository = "https://github.com/onecodex/finch-rs"
+license = "MIT"
+readme = "../README.md"
+edition = "2018"
+
+[dependencies]
+bincode = "1.2.0"
+capnp = "0.14"
+memmap = "0.7.0"
+murmurhash3 = "0.0.5"
+ndarray = "0.14"
+needletail = "0.4"
+numpy = { version = "0.13", optional = true }
+pyo3 = { version = "0.13", optional = true }
+rayon = "1.2.0"
+serde = { version = "1.0", features = ["derive"] }
+serde_json = "1"
+thiserror = "1"
+
+[dev-dependencies]
+predicates = "1"
+proptest = "0.10"
+
+[features]
+default = []
+python = ["numpy", "pyo3"]
+
+[lib]
+crate-type=["cdylib", "rlib"]
+name = "finch"
+
+[package.metadata.maturin]
+maintainer = "One Codex"
+classifier = ["Intended Audience :: Science/Research", "License :: OSI Approved :: MIT License", "Topic :: Scientific/Engineering :: Bio-Informatics"]
diff --git a/src/distance.rs b/lib/src/distance.rs
similarity index 100%
rename from src/distance.rs
rename to lib/src/distance.rs
diff --git a/src/errors.rs b/lib/src/errors.rs
similarity index 100%
rename from src/errors.rs
rename to lib/src/errors.rs
diff --git a/src/filtering.rs b/lib/src/filtering.rs
similarity index 100%
rename from src/filtering.rs
rename to lib/src/filtering.rs
diff --git a/src/lib.rs b/lib/src/lib.rs
similarity index 79%
rename from src/lib.rs
rename to lib/src/lib.rs
index 9a39f0b..8416dba 100644
--- a/src/lib.rs
+++ b/lib/src/lib.rs
@@ -1,12 +1,10 @@
-#[macro_use]
-extern crate serde_derive;
-
 use std::fs::File;
-use std::io::{BufReader, Read};
+use std::io::{stdin, BufReader, Read};
 use std::path::Path;
 
 use memmap::MmapOptions;
 use needletail::parse_fastx_reader;
+use needletail::parser::Format;
 use rayon::prelude::*;
 
 use crate::filtering::FilterParams;
@@ -14,7 +12,6 @@ use crate::serialization::{
     read_finch_file, read_mash_file, MultiSketch, Sketch, FINCH_BIN_EXT, FINCH_EXT, MASH_EXT,
 };
 use crate::sketch_schemes::SketchParams;
-use needletail::parser::Format;
 
 pub mod distance;
 pub mod filtering;
@@ -22,7 +19,6 @@ pub mod sketch_schemes;
 // it would be nice if there was a `pub(in main)` or something for
 // main_parsing so we don't import it for `lib` itself
 pub mod errors;
-pub mod main_parsing;
 #[cfg(feature = "python")]
 pub mod python;
 pub mod serialization;
@@ -38,16 +34,14 @@ pub fn sketch_files(
     let sketches: FinchResult<Vec<Sketch>> = filenames
         .par_iter()
         .map(|filename| {
-            // TODO: re-enable stdin if needed
             // open the file with a special case to handle stdin
-            // let sin = stdin();
-            // let reader: Box<dyn Read + Send> = if filename == &"-" {
-            //     Box::new(sin.lock())
-            // } else {
-            //     Box::new(File::open(&Path::new(filename))?)
-            // };
+            let reader: Box<dyn Read + Send> = if filename == &"-" {
+                // We're not locking it so technically not thread safe
+                Box::new(stdin())
+            } else {
+                Box::new(File::open(&Path::new(filename))?)
+            };
             // sketch!
-            let reader: Box<dyn Read + Send> = Box::new(File::open(&Path::new(filename))?);
             Ok(sketch_stream(reader, filename, sketch_params, &filters)?)
         })
         .collect();
@@ -99,8 +93,13 @@ pub fn sketch_stream<'a>(
     })
 }
 
-pub fn open_sketch_file(filename: &str) -> FinchResult<Vec<Sketch>> {
-    let file = File::open(filename).map_err(|_| format_err!("Error opening {}", &filename))?;
+pub fn open_sketch_file<P: AsRef<Path>>(path: P) -> FinchResult<Vec<Sketch>> {
+    let p = path.as_ref();
+    let filename = p
+        .file_name()
+        .ok_or_else(|| format_err!("Path does not have a filename: {:?}", p))?
+        .to_string_lossy();
+    let file = File::open(p).map_err(|_| format_err!("Error opening {:?}", p))?;
     if filename.ends_with(MASH_EXT) {
         let mut buf_reader = BufReader::new(file);
         read_mash_file(&mut buf_reader)
@@ -109,8 +108,8 @@ pub fn open_sketch_file(filename: &str) -> FinchResult<Vec<Sketch>> {
         read_finch_file(&mut buf_reader)
     } else if filename.ends_with(FINCH_EXT) || filename.ends_with(".json") {
         let mapped = unsafe { MmapOptions::new().map(&file)? };
-        let multisketch: MultiSketch = serde_json::from_slice(&mapped)
-            .map_err(|_| format_err!("Error parsing {}", &filename))?;
+        let multisketch: MultiSketch =
+            serde_json::from_slice(&mapped).map_err(|_| format_err!("Error parsing {:?}", &p))?;
         multisketch.to_sketches()
     } else {
         Err(format_err!("File suffix is not *.bsk, *.msh, or *.sk"))
diff --git a/src/python.rs b/lib/src/python.rs
similarity index 100%
rename from src/python.rs
rename to lib/src/python.rs
diff --git a/src/serialization/finch.capnp b/lib/src/serialization/finch.capnp
similarity index 100%
rename from src/serialization/finch.capnp
rename to lib/src/serialization/finch.capnp
diff --git a/src/serialization/finch_capnp.rs b/lib/src/serialization/finch_capnp.rs
similarity index 100%
rename from src/serialization/finch_capnp.rs
rename to lib/src/serialization/finch_capnp.rs
diff --git a/src/serialization/json.rs b/lib/src/serialization/json.rs
similarity index 98%
rename from src/serialization/json.rs
rename to lib/src/serialization/json.rs
index de70336..a26aa36 100644
--- a/src/serialization/json.rs
+++ b/lib/src/serialization/json.rs
@@ -2,8 +2,9 @@ use std::collections::HashMap;
 use std::fmt;
 use std::mem;
 
-use serde::de::{self, Deserialize, Deserializer, Visitor};
-use serde::ser::{Serialize, SerializeStruct, Serializer};
+use serde::de::{self, Deserializer, Visitor};
+use serde::ser::{SerializeStruct, Serializer};
+use serde::{Deserialize, Serialize};
 
 use crate::bail;
 use crate::errors::FinchResult;
diff --git a/src/serialization/mash.capnp b/lib/src/serialization/mash.capnp
similarity index 100%
rename from src/serialization/mash.capnp
rename to lib/src/serialization/mash.capnp
diff --git a/src/serialization/mash.rs b/lib/src/serialization/mash.rs
similarity index 100%
rename from src/serialization/mash.rs
rename to lib/src/serialization/mash.rs
diff --git a/src/serialization/mash_capnp.rs b/lib/src/serialization/mash_capnp.rs
similarity index 100%
rename from src/serialization/mash_capnp.rs
rename to lib/src/serialization/mash_capnp.rs
diff --git a/src/serialization/mod.rs b/lib/src/serialization/mod.rs
similarity index 99%
rename from src/serialization/mod.rs
rename to lib/src/serialization/mod.rs
index 2fc5c2c..94b31fe 100644
--- a/src/serialization/mod.rs
+++ b/lib/src/serialization/mod.rs
@@ -15,6 +15,7 @@ use std::io::{BufRead, Write};
 
 use capnp::message;
 use capnp::serialize as capnp_serialize;
+use serde::{Deserialize, Serialize};
 
 use crate::errors::FinchResult;
 use crate::filtering::FilterParams;
diff --git a/src/sketch_schemes/counts.rs b/lib/src/sketch_schemes/counts.rs
similarity index 100%
rename from src/sketch_schemes/counts.rs
rename to lib/src/sketch_schemes/counts.rs
diff --git a/src/sketch_schemes/hashing.rs b/lib/src/sketch_schemes/hashing.rs
similarity index 100%
rename from src/sketch_schemes/hashing.rs
rename to lib/src/sketch_schemes/hashing.rs
diff --git a/src/sketch_schemes/mash.rs b/lib/src/sketch_schemes/mash.rs
similarity index 100%
rename from src/sketch_schemes/mash.rs
rename to lib/src/sketch_schemes/mash.rs
diff --git a/src/sketch_schemes/mod.rs b/lib/src/sketch_schemes/mod.rs
similarity index 99%
rename from src/sketch_schemes/mod.rs
rename to lib/src/sketch_schemes/mod.rs
index 5b1c14c..3db41cd 100644
--- a/src/sketch_schemes/mod.rs
+++ b/lib/src/sketch_schemes/mod.rs
@@ -4,6 +4,7 @@ pub mod mash;
 pub mod scaled;
 
 use needletail::parser::SequenceRecord;
+use serde::{Deserialize, Serialize};
 
 use crate::bail;
 use crate::errors::FinchResult;
diff --git a/src/sketch_schemes/scaled.rs b/lib/src/sketch_schemes/scaled.rs
similarity index 100%
rename from src/sketch_schemes/scaled.rs
rename to lib/src/sketch_schemes/scaled.rs
diff --git a/src/statistics.rs b/lib/src/statistics.rs
similarity index 100%
rename from src/statistics.rs
rename to lib/src/statistics.rs
diff --git a/src/main.rs b/src/main.rs
deleted file mode 100644
index a479015..0000000
--- a/src/main.rs
+++ /dev/null
@@ -1,455 +0,0 @@
-#[macro_use]
-extern crate clap;
-extern crate finch;
-extern crate serde_json;
-
-use std::collections::{HashMap, HashSet};
-use std::error::Error;
-use std::fs::File;
-use std::io::{stderr, stdout, Write};
-use std::process::exit;
-
-use clap::{App, AppSettings, Arg, ArgMatches, SubCommand};
-
-use finch::distance::distance;
-use finch::errors::FinchResult;
-use finch::serialization::{
-    write_finch_file, write_mash_file, MultiSketch, Sketch, SketchDistance, FINCH_BIN_EXT,
-    FINCH_EXT, MASH_EXT,
-};
-use finch::statistics::{cardinality, hist};
-use finch::{bail, format_err, open_sketch_file, sketch_files};
-
-use finch::main_parsing::{
-    add_filter_options, add_sketch_options, get_float_arg, get_int_arg, parse_filter_options,
-    parse_sketch_options, update_sketch_params,
-};
-
-fn add_output_options<'a, 'b>(app: App<'a, 'b>) -> App<'a, 'b> {
-    app.arg(
-        Arg::with_name("output_file")
-            .short("o")
-            .long("output")
-            .help("Output to this file")
-            .takes_value(true),
-    )
-    .arg(
-        Arg::with_name("std_out")
-            .short("O")
-            .long("std-out")
-            .help("Output to stdout ('print to terminal')")
-            .conflicts_with("output_file"),
-    )
-}
-
-fn output_to<F>(output_fn: F, output: Option<&str>, extension: &str) -> FinchResult<()>
-where
-    F: Fn(&mut dyn Write) -> FinchResult<()>,
-{
-    match output {
-        None => {
-            let mut out = stdout();
-            output_fn(&mut out)?;
-        }
-        Some(o) => {
-            // if the filename doesn't have the right extension
-            // add it on
-            let filename = String::from(o);
-            let out_filename = if filename.ends_with(extension) {
-                filename
-            } else {
-                filename + extension
-            };
-
-            let mut out = File::create(&out_filename)
-                .map_err(|_| format_err!("Could not create {}", out_filename))?;
-            output_fn(&mut out)?;
-        }
-    };
-    Ok(())
-}
-
-fn main() {
-    // see https://github.com/rust-lang-nursery/failure/issues/76
-    if let Err(err) = run() {
-        let mut serr = stderr();
-        writeln!(serr, "Error: {:?}", err).expect("unable to write error to stderr");
-        let mut source = err.source();
-        while let Some(e) = source {
-            writeln!(serr, "Caused by: {:?}", e).expect("unable to write error to stderr");
-            source = e.source();
-        }
-        exit(1);
-    }
-}
-
-fn run() -> FinchResult<()> {
-    let mut sketch_command = SubCommand::with_name("sketch")
-        .about("Create sketches from FASTA/Q file(s)")
-        .arg(
-            Arg::with_name("INPUT")
-                .help("The file(s) to sketch")
-                .multiple(true)
-                .required(true),
-        )
-        .arg(
-            Arg::with_name("binary_format")
-                .short("b")
-                .long("finch-binary-format")
-                .help("Outputs sketch to a finch-native binary format"),
-        )
-        .arg(
-            Arg::with_name("mash_binary_format")
-                .short("B")
-                .long("mash-binary-format")
-                .conflicts_with("binary_format")
-                .help("Outputs sketch in a binary format compatible with `mash`"),
-        );
-    sketch_command = add_output_options(sketch_command);
-    sketch_command = add_filter_options(sketch_command);
-    sketch_command = add_sketch_options(sketch_command);
-
-    let mut dist_command = SubCommand::with_name("dist")
-        .about("Compute distances between sketches")
-        .arg(
-            Arg::with_name("INPUT")
-                .help("Sketchfile(s) to make comparisons for")
-                .multiple(true)
-                .required(true),
-        )
-        .arg(
-            Arg::with_name("pairwise")
-                .short("p")
-                .long("pairwise")
-                .conflicts_with("queries")
-                .help("Calculate distances between all sketches"),
-        )
-        .arg(
-            Arg::with_name("queries")
-                .short("q")
-                .long("queries")
-                .help("All distances are from these sketches (sketches must be in the first file)")
-                .multiple(true)
-                .conflicts_with("pairwise")
-                .takes_value(true),
-        )
-        .arg(
-            Arg::with_name("max_distance")
-                .short("d")
-                .long("max-dist")
-                .help("Only report distances under this threshold")
-                .default_value("1.0")
-                .takes_value(true),
-        )
-        .arg(
-            Arg::with_name("old_dist_mode")
-                .long("old-dist")
-                .help("Calculate distances using the old containment-biased Finch mode"),
-        );
-    dist_command = add_output_options(dist_command);
-    dist_command = add_filter_options(dist_command);
-    dist_command = add_sketch_options(dist_command);
-
-    let mut hist_command = SubCommand::with_name("hist")
-        .about("Display histograms of kmer abundances")
-        .arg(
-            Arg::with_name("INPUT")
-                .help("Generate histograms from these file(s)")
-                .multiple(true)
-                .required(true),
-        );
-    hist_command = add_output_options(hist_command);
-    hist_command = add_filter_options(hist_command);
-    hist_command = add_sketch_options(hist_command);
-
-    let mut info_command = SubCommand::with_name("info")
-        .about("Display basic statistics")
-        .arg(
-            Arg::with_name("INPUT")
-                .help("Return stats on these file(s)")
-                .multiple(true)
-                .required(true),
-        );
-    info_command = add_output_options(info_command);
-    info_command = add_filter_options(info_command);
-    info_command = add_sketch_options(info_command);
-
-    let matches = App::new("finch")
-        .version(crate_version!())
-        .author(crate_authors!())
-        .about("Tool for working with genomic MinHash sketches")
-        .setting(AppSettings::VersionlessSubcommands)
-        .setting(AppSettings::ArgRequiredElseHelp)
-        .subcommand(sketch_command)
-        .subcommand(dist_command)
-        .subcommand(hist_command)
-        .subcommand(info_command)
-        .get_matches();
-
-    if let Some(matches) = matches.subcommand_matches("sketch") {
-        let file_ext = if matches.is_present("binary_format") {
-            FINCH_BIN_EXT
-        } else if matches.is_present("mash_binary_format") {
-            MASH_EXT
-        } else {
-            FINCH_EXT
-        };
-        if matches.is_present("output_file") || matches.is_present("std_out") {
-            let sketches = parse_mash_files(matches)?;
-            let output = matches.value_of("output_file");
-
-            output_to(
-                |writer| {
-                    if matches.is_present("binary_format") {
-                        write_finch_file(writer, &sketches)?;
-                    } else if matches.is_present("mash_binary_format") {
-                        write_mash_file(writer, &sketches)?;
-                    } else {
-                        let multisketch = MultiSketch::from_sketches(&sketches)?;
-                        serde_json::to_writer(writer, &multisketch)?;
-                    }
-                    Ok(())
-                },
-                output,
-                &file_ext,
-            )?;
-        } else {
-            // special case for "sketching in place"
-            generate_sketch_files(matches, file_ext)?;
-        }
-    } else if let Some(matches) = matches.subcommand_matches("dist") {
-        let old_mode = matches.is_present("old_dist_mode");
-
-        let max_dist = get_float_arg(matches, "max_distance", 1f64)?;
-        let all_sketches = parse_mash_files(matches)?;
-
-        let mut query_sketches = Vec::new();
-        if matches.is_present("pairwise") {
-            for sketch in &all_sketches {
-                query_sketches.push(sketch);
-            }
-        } else if matches.is_present("queries") {
-            let query_names: HashSet<String> = matches
-                .values_of("queries")
-                .unwrap() // we already know it's present
-                .map(|s| s.to_string())
-                .collect();
-
-            for sketch in &all_sketches {
-                if query_names.contains(&sketch.name) {
-                    query_sketches.push(&sketch);
-                }
-            }
-        } else {
-            if all_sketches.is_empty() {
-                bail!("No sketches present!");
-            }
-            query_sketches.push(all_sketches.first().unwrap());
-        }
-
-        let distances = calc_sketch_distances(&query_sketches, &all_sketches, old_mode, max_dist);
-
-        output_to(
-            |writer| {
-                serde_json::to_writer(writer, &distances)
-                    .map_err(|_| format_err!("Could not serialize JSON to file"))?;
-                Ok(())
-            },
-            matches.value_of("output_file"),
-            ".json",
-        )?;
-    } else if let Some(matches) = matches.subcommand_matches("hist") {
-        let mut hist_map: HashMap<String, Vec<u64>> = HashMap::new();
-        let multisketch = parse_mash_files(matches)?;
-
-        for sketch in multisketch {
-            hist_map.insert(sketch.name.to_string(), hist(&sketch.hashes));
-        }
-
-        output_to(
-            |writer| {
-                serde_json::to_writer(writer, &hist_map)
-                    .map_err(|_| format_err!("Could not serialize JSON to file"))?;
-                Ok(())
-            },
-            matches.value_of("output_file"),
-            ".json",
-        )?;
-    } else if let Some(matches) = matches.subcommand_matches("info") {
-        // TODO: this should probably output JSON
-        let multisketch = parse_mash_files(matches)?;
-
-        for sketch in multisketch {
-            print!("{}", &sketch.name);
-            println!(" (from {}bp)", sketch.seq_length);
-            let kmers = &sketch.hashes;
-            if let Ok(c) = cardinality(kmers) {
-                println!("  Estimated # of Unique Kmers: {}", c);
-            }
-
-            let histogram = hist(kmers);
-            let mean = histogram
-                .iter()
-                .enumerate()
-                .map(|(i, v)| ((i as f32 + 1f32) * *v as f32, *v as f32))
-                .fold((0f32, 0f32), |e, s| (e.0 + s.0, e.1 + s.1));
-            println!("  Estimated Average Depth: {}x", mean.0 / mean.1);
-
-            let mut total_gc: u64 = 0;
-            for kmer in kmers {
-                total_gc += kmer
-                    .kmer
-                    .iter()
-                    .map(|b| match *b {
-                        b'G' | b'g' | b'C' | b'c' => u64::from(kmer.count),
-                        _ => 0,
-                    })
-                    .sum::<u64>();
-            }
-            let total_bases = if kmers.is_empty() {
-                0f32
-            } else {
-                mean.0 * kmers[0].kmer.len() as f32
-            };
-            println!(
-                "  Estimated % GC: {}%",
-                100f32 * total_gc as f32 / total_bases
-            );
-        }
-    }
-    Ok(())
-}
-
-fn generate_sketch_files(matches: &ArgMatches, file_ext: &str) -> FinchResult<()> {
-    let filenames: Vec<_> = matches
-        .values_of("INPUT")
-        .ok_or_else(|| format_err!("Bad INPUT"))?
-        .collect();
-
-    let kmer_length: u8 = get_int_arg(matches, "kmer_length")?;
-    let filters = parse_filter_options(matches, kmer_length)?;
-    let sketch_params = parse_sketch_options(matches, kmer_length, filters.filter_on)?;
-
-    for filename in filenames {
-        if filename.ends_with(".json")
-            || filename.ends_with(FINCH_EXT)
-            || filename.ends_with(FINCH_BIN_EXT)
-            || filename.ends_with(MASH_EXT)
-        {
-            bail!("Filename {} is not a sequence file?", filename);
-        }
-
-        let sketches = sketch_files(&[filename], &sketch_params, &filters)?;
-
-        let out_filename = filename.to_string() + file_ext;
-        let mut out = File::create(&out_filename)
-            .map_err(|_| format_err!("Could not open {}", out_filename))?;
-        if matches.is_present("binary_format") {
-            write_finch_file(&mut out, &sketches)?;
-        } else if matches.is_present("mash_binary_format") {
-            write_mash_file(&mut out, &sketches)?;
-        } else {
-            let multisketch: MultiSketch = MultiSketch::from_sketches(&sketches)?;
-            serde_json::to_writer(&mut out, &multisketch)?;
-        }
-    }
-    Ok(())
-}
-
-fn parse_mash_files(matches: &ArgMatches) -> FinchResult<Vec<Sketch>> {
-    let filenames: Vec<_> = matches
-        .values_of("INPUT")
-        .ok_or_else(|| format_err!("Bad INPUT"))?
-        .collect();
-
-    let mut sketch_filenames = Vec::new();
-    let mut seq_filenames = Vec::new();
-    for filename in filenames {
-        if filename.ends_with(".json")
-            || filename.ends_with(FINCH_EXT)
-            || filename.ends_with(FINCH_BIN_EXT)
-            || filename.ends_with(MASH_EXT)
-        {
-            sketch_filenames.push(filename);
-        } else {
-            seq_filenames.push(filename);
-        }
-    }
-
-    let kmer_length: u8 = get_int_arg(matches, "kmer_length")?;
-    let mut filters = parse_filter_options(matches, kmer_length)?;
-    let mut sketch_params = parse_sketch_options(matches, kmer_length, filters.filter_on)?;
-
-    let mut filename_iter = sketch_filenames.iter();
-    if let Some(first_filename) = filename_iter.next() {
-        let mut sketches = open_sketch_file(first_filename)?;
-
-        update_sketch_params(matches, &mut sketch_params, &sketches[0], first_filename)?;
-        // we also have to handle updating filter options separately because
-        // kmer_length changes how we calculate the `err_filter`
-        if matches.occurrences_of("kmer_length") == 0 {
-            filters = parse_filter_options(matches, sketch_params.k())?;
-        }
-
-        // now do the filtering for the first sketch file
-        if filters.filter_on == Some(true) {
-            for mut sketch in &mut sketches {
-                filters.filter_sketch(&mut sketch);
-            }
-        }
-
-        // and then handle the rest of the sketch files
-        for filename in filename_iter {
-            let extra_sketches = open_sketch_file(filename)?;
-            // check new sketches are compatible with original file
-            for sketch in &extra_sketches {
-                if let Some((name, v1, v2)) =
-                    sketch_params.check_compatibility(&sketch.sketch_params)
-                {
-                    bail!(
-                        "Sketch {} has {} {}, but working value is {}",
-                        sketch.name,
-                        name,
-                        v2,
-                        v1,
-                    );
-                }
-            }
-            sketches.extend(extra_sketches);
-            if filters.filter_on == Some(true) {
-                for mut sketch in &mut sketches {
-                    filters.filter_sketch(&mut sketch);
-                }
-            }
-        }
-
-        // now handle the sequences
-        let extra_sketches = sketch_files(&seq_filenames, &sketch_params, &filters)?;
-        sketches.extend(extra_sketches);
-        Ok(sketches)
-    } else {
-        // now handle the sequences
-        sketch_files(&seq_filenames, &sketch_params, &filters)
-    }
-}
-
-fn calc_sketch_distances(
-    query_sketches: &[&Sketch],
-    ref_sketches: &[Sketch],
-    old_mode: bool,
-    max_distance: f64,
-) -> Vec<SketchDistance> {
-    let mut distances = Vec::new();
-    for ref_sketch in ref_sketches {
-        for query_sketch in query_sketches {
-            if query_sketch == &ref_sketch {
-                continue;
-            }
-            let distance = distance(&query_sketch, &ref_sketch, old_mode).unwrap();
-            if distance.mash_distance <= max_distance {
-                distances.push(distance);
-            }
-        }
-    }
-    distances
-}


Test patch:
diff --git a/tests/data/query.fa b/cli/tests/data/query.fa
similarity index 100%
rename from tests/data/query.fa
rename to cli/tests/data/query.fa
diff --git a/tests/test_cli.rs b/cli/tests/test_cli.rs
similarity index 100%
rename from tests/test_cli.rs
rename to cli/tests/test_cli.rs