Repository: developmentseed/cql2-rs (language: python)

Issue:
Consolidate to one bin
I think it would be a simpler user experience and less maintenance for us to provide a single `cql2` bin instead of the current `cql2text` and `cql2json`. 


Discussion / hints:
I'm cool with that. At this point in time, really the bins were mostly there for me to do quick tests.

Fix patch:
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 4ad7907..22b6bfa 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -24,4 +24,4 @@ jobs:
       - name: cargo check
         run: cargo check
       - name: cargo test
-        run: cargo test
+        run: cargo test --package cql2
diff --git a/Cargo.lock b/Cargo.lock
index b400c9e..0ce4b6c 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -24,6 +24,55 @@ dependencies = [
  "memchr",
 ]
 
+[[package]]
+name = "anstream"
+version = "0.6.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "64e15c1ab1f89faffbf04a634d5e1962e9074f2741eef6d97f3c4e322426d526"
+dependencies = [
+ "anstyle",
+ "anstyle-parse",
+ "anstyle-query",
+ "anstyle-wincon",
+ "colorchoice",
+ "is_terminal_polyfill",
+ "utf8parse",
+]
+
+[[package]]
+name = "anstyle"
+version = "1.0.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1bec1de6f59aedf83baf9ff929c98f2ad654b97c9510f4e70cf6f661d49fd5b1"
+
+[[package]]
+name = "anstyle-parse"
+version = "0.2.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eb47de1e80c2b463c735db5b217a0ddc39d612e7ac9e2e96a5aed1f57616c1cb"
+dependencies = [
+ "utf8parse",
+]
+
+[[package]]
+name = "anstyle-query"
+version = "1.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6d36fc52c7f6c869915e99412912f22093507da8d9e942ceaf66fe4b7c14422a"
+dependencies = [
+ "windows-sys",
+]
+
+[[package]]
+name = "anstyle-wincon"
+version = "3.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5bf74e1b6e971609db8ca7a9ce79fd5768ab6ae46441c572e46cf596f59e57f8"
+dependencies = [
+ "anstyle",
+ "windows-sys",
+]
+
 [[package]]
 name = "appendlist"
 version = "1.4.0"
@@ -102,6 +151,52 @@ version = "1.0.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
 
+[[package]]
+name = "clap"
+version = "4.5.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7be5744db7978a28d9df86a214130d106a89ce49644cbc4e3f0c22c3fba30615"
+dependencies = [
+ "clap_builder",
+ "clap_derive",
+]
+
+[[package]]
+name = "clap_builder"
+version = "4.5.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a5fbc17d3ef8278f55b282b2a2e75ae6f6c7d4bb70ed3d0382375104bfafdb4b"
+dependencies = [
+ "anstream",
+ "anstyle",
+ "clap_lex",
+ "strsim",
+]
+
+[[package]]
+name = "clap_derive"
+version = "4.5.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4ac6a0c7b1a9e9a5186361f67dfa1b88213572f427fb9ab038efb2bd8c582dab"
+dependencies = [
+ "heck",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "clap_lex"
+version = "0.7.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1462739cb27611015575c0c11df5df7601141071f07518d56fcc1be504cbec97"
+
+[[package]]
+name = "colorchoice"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d3fd119d74b830634cea2a0f58bbd0d54540518a14397557951e79340abc28c0"
+
 [[package]]
 name = "cpufeatures"
 version = "0.2.14"
@@ -130,6 +225,15 @@ dependencies = [
  "thiserror",
 ]
 
+[[package]]
+name = "cql2-cli"
+version = "0.1.0"
+dependencies = [
+ "clap",
+ "cql2",
+ "serde_json",
+]
+
 [[package]]
 name = "crypto-common"
 version = "0.1.6"
@@ -340,6 +444,12 @@ version = "0.15.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "1e087f84d4f86bf4b218b927129862374b72199ae7d8657835f1e89000eea4fb"
 
+[[package]]
+name = "heck"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
+
 [[package]]
 name = "idna"
 version = "0.5.0"
@@ -360,6 +470,12 @@ dependencies = [
  "hashbrown",
 ]
 
+[[package]]
+name = "is_terminal_polyfill"
+version = "1.70.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf"
+
 [[package]]
 name = "itoa"
 version = "1.0.11"
@@ -408,12 +524,9 @@ dependencies = [
 
 [[package]]
 name = "once_cell"
-version = "1.20.1"
+version = "1.20.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "82881c4be219ab5faaf2ad5e5e5ecdff8c66bd7402ca3160975c93b24961afd1"
-dependencies = [
- "portable-atomic",
-]
+checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775"
 
 [[package]]
 name = "percent-encoding"
@@ -478,12 +591,6 @@ version = "0.1.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
 
-[[package]]
-name = "portable-atomic"
-version = "1.9.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cc9c68a3f6da06753e9335d63e27f6b9754dd1920d941135b7ea8224f141adb2"
-
 [[package]]
 name = "proc-macro-crate"
 version = "3.2.0"
@@ -650,6 +757,12 @@ dependencies = [
  "autocfg",
 ]
 
+[[package]]
+name = "strsim"
+version = "0.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
+
 [[package]]
 name = "syn"
 version = "2.0.79"
@@ -757,6 +870,12 @@ dependencies = [
  "percent-encoding",
 ]
 
+[[package]]
+name = "utf8parse"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
+
 [[package]]
 name = "version_check"
 version = "0.9.5"
@@ -769,6 +888,79 @@ version = "0.11.0+wasi-snapshot-preview1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
 
+[[package]]
+name = "windows-sys"
+version = "0.52.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
+dependencies = [
+ "windows-targets",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
+dependencies = [
+ "windows_aarch64_gnullvm",
+ "windows_aarch64_msvc",
+ "windows_i686_gnu",
+ "windows_i686_gnullvm",
+ "windows_i686_msvc",
+ "windows_x86_64_gnu",
+ "windows_x86_64_gnullvm",
+ "windows_x86_64_msvc",
+]
+
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
+
+[[package]]
+name = "windows_i686_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
+
 [[package]]
 name = "winnow"
 version = "0.6.20"
diff --git a/Cargo.toml b/Cargo.toml
index cc11957..131cf91 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -20,14 +20,6 @@ thiserror = "1.0"
 assert-json-diff = "2"
 rstest = "0.23"
 
-[[bin]]
-name = "cql2json"
-path = "src/bin/cql2json.rs"
-test = false
-doc = false
-
-[[bin]]
-name = "cql2text"
-path = "src/bin/cql2text.rs"
-test = false
-doc = false
+[workspace]
+default-members = ["cli"]
+members = ["cli"]
diff --git a/README.md b/README.md
index 96e6f1e..bb41cff 100644
--- a/README.md
+++ b/README.md
@@ -5,13 +5,65 @@
 Parse, validate, and convert CQL2-Text and CQL2-JSON.
 
 ## CLI
-Both commands take either CQL2-Text or CQL2-JSON on stdin, as a quoted/escaped argument, or interactively. They will return status code 0 on successful validation or status code 1 if there was a problem parsing or validating. Verbosity of the validation errors can be controlled using the CQL2_DEBUG_LEVEL environment variable between 0 and 3.
-- cql2json - returns standardized CQL2-JSON
-- cql2text - returns standardized CQL2-Text
+
+At its simplest, the command-line interface (CLI) is a pass-through validator:
+
+```shell
+$ cql2 < tests/fixtures/text/example01.txt # will succeed if the CQL2 is valid
+("landsat:scene_id" = 'LC82030282019133LGN00')
+```
+
+You can convert formats:
+
+```shell
+$ cql2 -o json < tests/fixtures/text/example01.txt
+{"op":"=","args":[{"property":"landsat:scene_id"},"LC82030282019133LGN00"]}
+```
+
+Use `-v` to get detailed validation information:
+
+```shell
+$ cql2 'wrong' -v  
+[ERROR] Invalid CQL2: wrong
+For more detailed validation information, use -vv
+jsonschema validation failed with file:///tmp/cql2.json#
+- at '': oneOf failed, none matched
+  - at '': missing properties 'op', 'args'
+  - at '': missing properties 'op', 'args'
+  - at '': oneOf failed, none matched
+    - at '': missing properties 'op', 'args'
+    - at '': missing properties 'op', 'args'
+    - at '': missing properties 'op', 'args'
+    - at '': missing properties 'op', 'args'
+    - at '': missing properties 'op', 'args'
+  - at '': missing properties 'op', 'args'
+  - at '': missing properties 'op', 'args'
+  - at '': missing properties 'op', 'args'
+  - at '': missing properties 'op', 'args'
+  - at '': want boolean, but got object
+```
+
+cql2-text parsing errors are pretty-printed:
+
+```shell
+$ cql2 '(foo ~= "bar")' 
+[ERROR] Parsing error: (foo ~= "bar")
+ --> 1:6
+  |
+1 | (foo ~= "bar")
+  |      ^---
+  |
+  = expected NotFlag, And, Or, ConcatInfixOp, Add, Subtract, Multiply, Divide, Modulo, Power, Eq, Gt, GtEq, Lt, LtEq, NotEq, Is, or IsNullPostfix
+```
+
+Use `cql2 --help` to get a complete listing of the CLI arguments and formats.
 
 ## Response
-Response may not match the input.
+
+Responses may not match the input.
+
 ### CQL2-Text Differences
+
 - all identifiers in output are double quoted
 - position of "NOT" keywords is standardized to be before the expression (ie "... NOT LIKE ..." will become "NOT ... LIKE ..."
 - The Negative operator on anything besides a literal number becomes "* -1"
@@ -19,5 +71,5 @@ Response may not match the input.
 
 Tasks to get to ready-to-use state:
 - [x] Parse all examples from CQL2 examples into json that passes json schema validation.
-- [ ] Add tests that compare OGC examples to parsed/standardized/validated CQL2-Text and CQL2-JSON
+- [x] Add tests that compare OGC examples to parsed/standardized/validated CQL2-Text and CQL2-JSON
 - [ ] Fix issues with Z, ZM, and M WKT variants
diff --git a/cli/Cargo.toml b/cli/Cargo.toml
new file mode 100644
index 0000000..36e0756
--- /dev/null
+++ b/cli/Cargo.toml
@@ -0,0 +1,15 @@
+[package]
+name = "cql2-cli"
+version = "0.1.0"
+edition = "2021"
+
+[dependencies]
+cql2 = { path = "..", version = "0.1.0" }
+clap = { version = "4.5", features = ["derive"] }
+serde_json = "1.0"
+
+[[bin]]
+name = "cql2"
+path = "src/main.rs"
+test = false
+doc = false
diff --git a/cli/src/main.rs b/cli/src/main.rs
new file mode 100644
index 0000000..8744f18
--- /dev/null
+++ b/cli/src/main.rs
@@ -0,0 +1,121 @@
+use clap::{ArgAction, Parser, ValueEnum};
+use cql2::{Expr, Validator};
+use std::io::Read;
+
+#[derive(Debug, Parser)]
+struct Cli {
+    /// The input CQL2
+    ///
+    /// If not provided, or `-`, the CQL2 will be read from standard input. The
+    /// type (json or text) will be auto-detected. To specify a format, use
+    /// --input-format.
+    input: Option<String>,
+
+    /// The input format.
+    ///
+    /// If not provided, the format will be auto-detected from the input.
+    #[arg(short, long)]
+    input_format: Option<InputFormat>,
+
+    /// The output format.
+    ///
+    /// If not provided, the format will be the same as the input.
+    #[arg(short, long)]
+    output_format: Option<OutputFormat>,
+
+    /// Validate the CQL2
+    #[arg(long, default_value_t = true, action = ArgAction::Set)]
+    validate: bool,
+
+    /// Verbosity.
+    ///
+    /// Provide this argument several times to turn up the chatter.
+    #[arg(short, long, action = ArgAction::Count)]
+    verbose: u8,
+}
+
+#[derive(Debug, ValueEnum, Clone)]
+enum InputFormat {
+    /// cql2-json
+    Json,
+
+    /// cql2-text
+    Text,
+}
+
+#[derive(Debug, ValueEnum, Clone)]
+enum OutputFormat {
+    /// cql2-json, pretty-printed
+    JsonPretty,
+
+    /// cql2-json, compact
+    Json,
+
+    /// cql2-text
+    Text,
+
+    /// SQL
+    Sql,
+}
+
+fn main() {
+    let cli = Cli::parse();
+    let input = cli
+        .input
+        .and_then(|input| if input == "-" { None } else { Some(input) })
+        .unwrap_or_else(read_stdin);
+    let input_format = cli.input_format.unwrap_or_else(|| {
+        if input.starts_with('{') {
+            InputFormat::Json
+        } else {
+            InputFormat::Text
+        }
+    });
+    let expr: Expr = match input_format {
+        InputFormat::Json => cql2::parse_json(&input).unwrap(),
+        InputFormat::Text => match cql2::parse_text(&input) {
+            Ok(expr) => expr,
+            Err(err) => {
+                eprintln!("[ERROR] Parsing error: {input}");
+                eprintln!("{err}");
+                std::process::exit(1)
+            }
+        },
+    };
+    if cli.validate {
+        let validator = Validator::new().unwrap();
+        let value = serde_json::to_value(&expr).unwrap();
+        if let Err(error) = validator.validate(&value) {
+            eprintln!("[ERROR] Invalid CQL2: {input}");
+            match cli.verbose {
+                0 => eprintln!("For more detailed validation information, use -v"),
+                1 => eprintln!("For more detailed validation information, use -vv\n{error}"),
+                2 => eprintln!("For more detailed validation information, use -vvv\n{error:#}"),
+                _ => {
+                    let detailed_output = error.detailed_output();
+                    eprintln!("{detailed_output:#}");
+                }
+            }
+            std::process::exit(1)
+        }
+    }
+    let output_format = cli.output_format.unwrap_or_else(|| match input_format {
+        InputFormat::Json => OutputFormat::Json,
+        InputFormat::Text => OutputFormat::Text,
+    });
+    match output_format {
+        OutputFormat::JsonPretty => serde_json::to_writer_pretty(std::io::stdout(), &expr).unwrap(),
+        OutputFormat::Json => serde_json::to_writer(std::io::stdout(), &expr).unwrap(),
+        OutputFormat::Text => print!("{}", expr.to_text().unwrap()),
+        OutputFormat::Sql => {
+            serde_json::to_writer_pretty(std::io::stdout(), &expr.to_sql().unwrap()).unwrap()
+        }
+    }
+    println!()
+}
+
+fn read_stdin() -> String {
+    let mut buf = String::new();
+    std::io::stdin().read_to_string(&mut buf).unwrap();
+    buf
+}
diff --git a/src/bin/cql2json.rs b/src/bin/cql2json.rs
deleted file mode 100644
index cd8852b..0000000
--- a/src/bin/cql2json.rs
+++ /dev/null
@@ -1,9 +0,0 @@
-use cql2::parse_stdin;
-
-fn main() {
-    if let Ok(parsed) = parse_stdin() {
-        println!("{}", parsed.to_json().unwrap());
-    } else {
-        std::process::exit(1)
-    }
-}
diff --git a/src/bin/cql2text.rs b/src/bin/cql2text.rs
deleted file mode 100644
index 4c97a2d..0000000
--- a/src/bin/cql2text.rs
+++ /dev/null
@@ -1,9 +0,0 @@
-use cql2::parse_stdin;
-
-fn main() {
-    if let Ok(parsed) = parse_stdin() {
-        println!("{}", parsed.to_text().unwrap());
-    } else {
-        std::process::exit(1)
-    }
-}
diff --git a/src/lib.rs b/src/lib.rs
index fc9cbfb..3549951 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -9,7 +9,7 @@ mod parser;
 mod validator;
 
 use serde_derive::{Deserialize, Serialize};
-use std::{fs, io::Read, path::Path};
+use std::{fs, path::Path};
 pub use {error::Error, expr::Expr, geometry::Geometry, parser::parse_text, validator::Validator};
 
 /// A SQL query, broken into the query and parameters.
@@ -43,74 +43,5 @@ pub fn parse_file(path: impl AsRef<Path>) -> Result<Expr, Error> {
     s.parse()
 }
 
-fn get_stdin() -> Result<String, std::io::Error> {
-    use std::{
-        env,
-        io::{self, IsTerminal},
-    };
-    let args: Vec<String> = env::args().collect();
-    let mut buffer = String::new();
-
-    if args.len() >= 2 {
-        buffer = args[1].to_string();
-    } else if io::stdin().is_terminal() {
-        println!("Enter CQL2 as Text or JSON, then hit return");
-        io::stdin().read_line(&mut buffer)?;
-    } else {
-        io::stdin().read_to_string(&mut buffer)?;
-    }
-    Ok(buffer)
-}
-
-fn parse_stderr(s: &str) -> Result<Expr, Error> {
-    let debug_level: u8 = std::env::var("CQL2_DEBUG_LEVEL")
-        .ok()
-        .map(|s| s.parse())
-        .transpose()?
-        .unwrap_or(1);
-    let validator = Validator::new().unwrap();
-
-    let parsed: Expr = s.parse()?;
-    let value = serde_json::to_value(&parsed)?;
-
-    let validation = validator.validate(&value);
-
-    match validation {
-        Ok(()) => Ok(parsed),
-        Err(err) => {
-            eprintln!("Passed in CQL2 parsed to {value}.");
-            eprintln!("This did not pass jsonschema validation for CQL2.");
-            match debug_level {
-                0 => eprintln!("For more detailed validation details set CQL2_DEBUG_LEVEL to 1."),
-                1 => eprintln!(
-                    "{err}\nFor more detailed validation details set CQL2_DEBUG_LEVEL to 2."
-                ),
-                2 => eprintln!(
-                    "{err:#}\nFor more detailed validation details set CQL2_DEBUG_LEVEL to 3."
-                ),
-                _ => {
-                    let detailed_output = err.detailed_output();
-                    eprintln!("{detailed_output:#}");
-                }
-            }
-            Err(Error::Validation(serde_json::to_value(
-                err.detailed_output(),
-            )?))
-        }
-    }
-}
-
-/// Parse standard input into a CQL2 expression.
-///
-/// # Examples
-///
-/// ```no_run
-/// let expr = cql2::parse_stdin();
-/// ```
-pub fn parse_stdin() -> Result<Expr, Error> {
-    let buffer = get_stdin()?;
-    parse_stderr(&buffer)
-}
-
 #[cfg(test)]
 use {assert_json_diff as _, rstest as _};


Test patch:
diff --git a/tests/generate-expected b/tests/generate-expected
index f079a3a..36f9bab 100755
--- a/tests/generate-expected
+++ b/tests/generate-expected
@@ -20,10 +20,8 @@ for input_format in json text; do
     for path in "$fixtures/$input_format"/*.$input_extension; do
         for output_format in json text; do
             if [ $output_format = "json" ]; then
-                bin="target/debug/cql2json"
                 output_extension="json"
             else
-                bin="target/debug/cql2text"
                 output_extension="txt"
             fi
 
@@ -31,7 +29,7 @@ for input_format in json text; do
             file_stem="${file_name%.*}"
             mkdir -p "$expected/$output_format"
 
-            $bin <$path > "$expected/$input_format/$file_stem.$output_extension"
+            target/debug/cql2 --output-format $output_format <$path > "$expected/$input_format/$file_stem.$output_extension"
         done
     done
 done