From 95c73dd9fdf507fbc167018c33b3c832dbe1a0db Mon Sep 17 00:00:00 2001
From: bin liu <liubin0329@gmail.com>
Date: Wed, 23 Nov 2022 21:10:49 +0800
Subject: [PATCH 1/7] add `check` sub command to check if a key exists

It will check if the key exists.

Signed-off-by: bin liu <liubin0329@gmail.com>
---
 README.md   | 40 +++++++++++++++++++++++++++++++++++++---
 src/main.rs | 49 +++++++++++++++++++++++++++++++++++++++++++++----
 2 files changed, 82 insertions(+), 7 deletions(-)

diff --git a/README.md b/README.md
index 7f7d25d..a982f29 100644
--- a/README.md
+++ b/README.md
@@ -100,9 +100,43 @@ FLAGS:
     -V, --version    Prints version information
 
 SUBCOMMANDS:
-    get     Print some data from the file
-    help    Prints this message or the help of the given subcommand(s)
-    set     Edit the file to set some data (currently, just print modified version)
+    check    Check if a key exists
+    get      Print some data from the file
+    help     Prints this message or the help of the given subcommand(s)
+    set      Edit the file to set some data (currently, just print modified version)
+```
+
+### `toml check`
+
+```
+$ toml check --help
+toml-check 0.2.0
+Check if a key exists
+
+USAGE:
+    toml check <path> <query>
+
+FLAGS:
+    -h, --help       Prints help information
+    -V, --version    Prints version information
+
+ARGS:
+    <path>     Path to the TOML file to read
+    <query>    Query within the TOML data (e.g. `dependencies.serde`, `foo[0].bar`)
+```
+
+Check whether a key exists. It will print `true` to stdout in case exists, and set exit code to `0`,
+otherwise it will print `false` to stderr and set exit code to `1`.
+
+```sh
+$ toml check test.toml plugins.name2
+flase
+$ echo $?
+1
+$ toml check test.toml plugins.name
+true
+$ echo $?
+0
 ```
 
 ### `toml get`
diff --git a/src/main.rs b/src/main.rs
index 0d1c10f..c9d5185 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -10,11 +10,20 @@ use structopt::StructOpt;
 use toml_edit::{value, Document, Item, Table, Value};
 
 use query_parser::{parse_query, Query, TpathSegment};
+use TpathSegment::{Name, Num};
 
 // TODO: Get more of the description in the README into the CLI help.
 #[derive(StructOpt)]
 #[structopt(about)]
 enum Args {
+    /// Check if a key exists
+    Check {
+        /// Path to the TOML file to read
+        #[structopt(parse(from_os_str))]
+        path: PathBuf,
+        /// Query within the TOML data (e.g. `dependencies.serde`, `foo[0].bar`)
+        query: String,
+    },
     /// Print some data from the file
     Get {
         /// Path to the TOML file to read
@@ -58,6 +67,7 @@ enum CliError {
 fn main() -> Result<(), Error> {
     let args = Args::from_args();
     match args {
+        Args::Check { path, query } => check(path, &query),
         Args::Get { path, query, opts } => get(path, &query, opts)?,
         Args::Set {
             path,
@@ -75,6 +85,41 @@ fn read_parse(path: PathBuf) -> Result<Document, Error> {
     Ok(data.parse::<Document>()?)
 }
 
+fn check_exists(path: PathBuf, query: &str) -> Result<bool, Error> {
+    let tpath = parse_query_cli(query)?.0;
+    let doc = read_parse(path)?;
+    let mut item = doc.as_item();
+
+    for seg in tpath {
+        match seg {
+            Name(n) => {
+                let i = item.get(n);
+                if i.is_none() {
+                    return Ok(false);
+                }
+                item = i.unwrap();
+            }
+            Num(n) => item = &item[n],
+        }
+    }
+
+    Ok(true)
+}
+
+/// Check whether a key exists.
+/// It will print 'true' to stdout in case exists, and set exit code to '0'
+/// otherwise it will print 'false' to stderr and set exit code to '1'
+fn check(path: PathBuf, query: &str) {
+    if let Ok(r) = check_exists(path, query) {
+        if r {
+            println!("true");
+            std::process::exit(0);
+        }
+    }
+    eprintln!("flase");
+    std::process::exit(1);
+}
+
 fn get(path: PathBuf, query: &str, opts: GetOpts) -> Result<(), Error> {
     let tpath = parse_query_cli(query)?.0;
     let doc = read_parse(path)?;
@@ -90,8 +135,6 @@ fn get(path: PathBuf, query: &str, opts: GetOpts) -> Result<(), Error> {
 }
 
 fn print_toml_fragment(doc: &Document, tpath: &[TpathSegment]) {
-    use TpathSegment::{Name, Num};
-
     let mut item = doc.as_item();
     let mut breadcrumbs = vec![];
     for seg in tpath {
@@ -141,7 +184,6 @@ fn set(path: PathBuf, query: &str, value_str: &str) -> Result<(), Error> {
     let mut item = doc.as_item_mut();
     let mut already_inline = false;
     let mut tpath = &tpath[..];
-    use TpathSegment::{Name, Num};
     while let Some(seg) = tpath.first() {
         tpath = &tpath[1..]; // TODO simplify to `for`, unless end up needing a tail
         match seg {
@@ -192,7 +234,6 @@ fn parse_query_cli(query: &str) -> Result<Query, CliError> {
 }
 
 fn walk_tpath<'a>(mut item: &'a toml_edit::Item, tpath: &[TpathSegment]) -> &'a toml_edit::Item {
-    use TpathSegment::{Name, Num};
     for seg in tpath {
         match seg {
             Name(n) => item = &item[n],

From 652f864decf893814add3b48221ee5505f0139fe Mon Sep 17 00:00:00 2001
From: bin liu <liubin0329@gmail.com>
Date: Wed, 23 Nov 2022 22:06:18 +0800
Subject: [PATCH 2/7] add realse action to release tarball for downloading

Signed-off-by: bin liu <liubin0329@gmail.com>
---
 .github/workflows/release.yml | 85 +++++++++++++++++++++++++++++++++++
 1 file changed, 85 insertions(+)
 create mode 100644 .github/workflows/release.yml

diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
new file mode 100644
index 0000000..dc4109e
--- /dev/null
+++ b/.github/workflows/release.yml
@@ -0,0 +1,85 @@
+name: release
+
+on:
+  push:
+    tags:
+     - v[0-9]+.[0-9]+.[0-9]+*
+  workflow_dispatch:
+
+env:
+  CARGO_TERM_COLOR: always
+  RUST_TARGET: x86_64-unknown-linux-musl
+
+jobs:
+  build-linux:
+    runs-on: ubuntu-latest
+    steps:
+    - uses: actions/checkout@v2
+    - name: Build toml cli
+      # uses: juankaram/rust-musl-action@master
+      uses: gmiam/rust-musl-action@master
+      with:
+        args: cargo build --target $RUST_TARGET --release
+    - name: store-artifacts
+      uses: actions/upload-artifact@v2
+      with:
+        if-no-files-found: error
+        name: toml-artifacts-linux
+        path: |
+          target/${{ env.RUST_TARGET }}/release/toml
+
+  prepare-tarball-linux:
+    runs-on: ubuntu-latest
+    needs: [build-linux]
+    steps:
+    - name: download artifacts
+      uses: actions/download-artifact@v2
+      with:
+        name: toml-artifacts-linux
+        path: toml-cli
+    - name: prepare release tarball
+      run: |
+        tag=$(echo $GITHUB_REF | cut -d/ -f3-)
+        tarball="toml-cli-$tag-linux-amd64.tgz"
+        chmod +x toml-cli/*
+        tar cf - toml-cli | gzip > ${tarball}
+        echo "tarball=${tarball}" >> $GITHUB_ENV
+
+        shasum="$tarball.sha256sum"
+        sha256sum $tarball > $shasum
+        echo "tarball_shasum=${shasum}" >> $GITHUB_ENV
+    - name: store-artifacts
+      uses: actions/upload-artifact@v2
+      with:
+        name: release-tarball
+        path: |
+          ${{ env.tarball }}
+          ${{ env.tarball_shasum }}
+
+  create-release:
+    runs-on: ubuntu-latest
+    needs: [prepare-tarball-linux]
+    steps:
+    - name: download artifacts
+      uses: actions/download-artifact@v2
+      with:
+        name: release-tarball
+        path: tarballs
+    - name: prepare release env
+      run: |
+        echo "tarballs<<EOF" >> $GITHUB_ENV
+        for I in $(ls tarballs);do echo "tarballs/${I}" >> $GITHUB_ENV; done
+        echo "EOF" >> $GITHUB_ENV
+        tag=$(echo $GITHUB_REF | cut -d/ -f3-)
+        echo "tag=${tag}" >> $GITHUB_ENV
+        cat $GITHUB_ENV
+    - name: push release
+      if: github.event_name == 'push'
+      uses: softprops/action-gh-release@v1
+      with:
+        name: "Toml cli ${{ env.tag }}"
+        body: |
+          "Toml cli release ${{ env.tag }}"
+        generate_release_notes: true
+        files: |
+          ${{ env.tarballs }}

From d695203c07435ca688bd49b5eac7e299caeca69b Mon Sep 17 00:00:00 2001
From: bin liu <liubin0329@gmail.com>
Date: Thu, 24 Nov 2022 11:39:50 +0800
Subject: [PATCH 3/7] add Dockerfile to release container image

Signed-off-by: bin liu <liubin0329@gmail.com>
---
 .github/workflows/release.yml | 33 +++++++++++++++++++++++++++++++++
 .gitignore                    |  1 +
 misc/Dockerfile               |  4 ++++
 3 files changed, 38 insertions(+)
 create mode 100644 misc/Dockerfile

diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index dc4109e..46ec6c6 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -9,6 +9,8 @@ on:
 env:
   CARGO_TERM_COLOR: always
   RUST_TARGET: x86_64-unknown-linux-musl
+  REGISTRY: ghcr.io
+  IMAGE_NAME: ${{ github.repository }}
 
 jobs:
   build-linux:
@@ -83,3 +85,34 @@ jobs:
         generate_release_notes: true
         files: |
           ${{ env.tarballs }}
+
+  publish-image:
+    runs-on: ubuntu-latest
+    needs: [build-linux]
+    steps:
+      - name: Checkout repository
+        uses: actions/checkout@v2
+      - name: Log in to the container registry
+        uses: docker/login-action@v2
+        with:
+          registry: ${{ env.REGISTRY }}
+          username: ${{ github.actor }}
+          password: ${{ secrets.GITHUB_TOKEN }}
+      - name: download artifacts
+        uses: actions/download-artifact@v2
+        with:
+          name: toml-artifacts-linux
+          path: misc
+      - name: Extract metadata (tags, labels) for Docker
+        id: meta
+        uses: docker/metadata-action@v4
+        with:
+          images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
+      - name: build and push toml cli image
+        uses: docker/build-push-action@v3
+        with:
+          context: misc
+          file: misc/Dockerfile
+          push: true
+          tags: ${{ steps.meta.outputs.tags }}
+          labels: ${{ steps.meta.outputs.labels }}
diff --git a/.gitignore b/.gitignore
index 53eaa21..346d12b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,2 +1,3 @@
 /target
 **/*.rs.bk
+toml
diff --git a/misc/Dockerfile b/misc/Dockerfile
new file mode 100644
index 0000000..11df8c4
--- /dev/null
+++ b/misc/Dockerfile
@@ -0,0 +1,4 @@
+FROM alpine:3.17
+
+ADD toml /bin/toml
+RUN chmod +x /bin/toml

From 783e1a22bcbd3c776fd7e94ab0b21b20767688cb Mon Sep 17 00:00:00 2001
From: bin liu <liubin0329@gmail.com>
Date: Thu, 24 Nov 2022 14:33:25 +0800
Subject: [PATCH 4/7] support set different data type

now support set bool/i64.

Signed-off-by: bin liu <liubin0329@gmail.com>
---
 src/main.rs | 12 +++++++++++-
 1 file changed, 11 insertions(+), 1 deletion(-)

diff --git a/src/main.rs b/src/main.rs
index c9d5185..5cc29e2 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -220,13 +220,23 @@ fn set(path: PathBuf, query: &str, value_str: &str) -> Result<(), Error> {
             }
         }
     }
-    *item = value(value_str);
+    *item = detect_value(value_str);
 
     // TODO actually write back
     print!("{}", doc);
     Ok(())
 }
 
+fn detect_value(value_str: &str) -> Item {
+    if let Ok(i) = value_str.parse::<i64>() {
+        value(i)
+    } else if let Ok(b) = value_str.parse::<bool>() {
+        value(b)
+    } else {
+        value(value_str)
+    }
+}
+
 fn parse_query_cli(query: &str) -> Result<Query, CliError> {
     parse_query(query).map_err(|_err| {
         CliError::BadQuery() // TODO: specific message

From a99be312ac7c7293c11f119f50b8bd0d79e0d9fa Mon Sep 17 00:00:00 2001
From: bin liu <liubin0329@gmail.com>
Date: Thu, 24 Nov 2022 14:35:30 +0800
Subject: [PATCH 5/7] fix type: flase -> false

Signed-off-by: bin liu <liubin0329@gmail.com>
---
 README.md   | 2 +-
 src/main.rs | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/README.md b/README.md
index a982f29..297fe6b 100644
--- a/README.md
+++ b/README.md
@@ -130,7 +130,7 @@ otherwise it will print `false` to stderr and set exit code to `1`.
 
 ```sh
 $ toml check test.toml plugins.name2
-flase
+false
 $ echo $?
 1
 $ toml check test.toml plugins.name
diff --git a/src/main.rs b/src/main.rs
index 5cc29e2..dff67f0 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -116,7 +116,7 @@ fn check(path: PathBuf, query: &str) {
             std::process::exit(0);
         }
     }
-    eprintln!("flase");
+    eprintln!("false");
     std::process::exit(1);
 }
 

From 63aa4c4f35191134ef79cb5c2598fa73069d7d28 Mon Sep 17 00:00:00 2001
From: bin liu <liubin0329@gmail.com>
Date: Thu, 24 Nov 2022 16:00:35 +0800
Subject: [PATCH 6/7] support edit file in place

add two opitons for set sub command:

- overwrite: save the result to the TOML file.
- backup: create a backup file before overwrite.

Both of them are false by default.

Signed-off-by: bin liu <liubin0329@gmail.com>
---
 Cargo.lock  | 262 ++++++++++++++++++++++++++++++++++++++++++++++++++++
 Cargo.toml  |   1 +
 README.md   |  10 +-
 src/main.rs |  39 ++++++--
 4 files changed, 302 insertions(+), 10 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock
index 78f01d3..77638a6 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -17,6 +17,15 @@ version = "1.0.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
 
+[[package]]
+name = "android_system_properties"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
+dependencies = [
+ "libc",
+]
+
 [[package]]
 name = "ansi_term"
 version = "0.12.1"
@@ -70,6 +79,12 @@ version = "1.3.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
 
+[[package]]
+name = "bumpalo"
+version = "3.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "572f695136211188308f16ad2ca5c851a712c464060ae6974944458eb83880ba"
+
 [[package]]
 name = "bytes"
 version = "1.3.0"
@@ -88,6 +103,21 @@ version = "1.0.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
 
+[[package]]
+name = "chrono"
+version = "0.4.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "16b0a3d9ed01224b22057780a37bb8c5dbfe1be8ba48678e7bf57ec4b385411f"
+dependencies = [
+ "iana-time-zone",
+ "js-sys",
+ "num-integer",
+ "num-traits",
+ "time",
+ "wasm-bindgen",
+ "winapi",
+]
+
 [[package]]
 name = "clap"
 version = "2.34.0"
@@ -103,6 +133,16 @@ dependencies = [
  "vec_map",
 ]
 
+[[package]]
+name = "codespan-reporting"
+version = "0.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3538270d33cc669650c4b093848450d380def10c331d38c768e34cac80576e6e"
+dependencies = [
+ "termcolor",
+ "unicode-width",
+]
+
 [[package]]
 name = "combine"
 version = "4.6.6"
@@ -113,6 +153,56 @@ dependencies = [
  "memchr",
 ]
 
+[[package]]
+name = "core-foundation-sys"
+version = "0.8.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc"
+
+[[package]]
+name = "cxx"
+version = "1.0.82"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d4a41a86530d0fe7f5d9ea779916b7cadd2d4f9add748b99c2c029cbbdfaf453"
+dependencies = [
+ "cc",
+ "cxxbridge-flags",
+ "cxxbridge-macro",
+ "link-cplusplus",
+]
+
+[[package]]
+name = "cxx-build"
+version = "1.0.82"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "06416d667ff3e3ad2df1cd8cd8afae5da26cf9cec4d0825040f88b5ca659a2f0"
+dependencies = [
+ "cc",
+ "codespan-reporting",
+ "once_cell",
+ "proc-macro2",
+ "quote",
+ "scratch",
+ "syn",
+]
+
+[[package]]
+name = "cxxbridge-flags"
+version = "1.0.82"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "820a9a2af1669deeef27cb271f476ffd196a2c4b6731336011e0ba63e2c7cf71"
+
+[[package]]
+name = "cxxbridge-macro"
+version = "1.0.82"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a08a6e2fcc370a089ad3b4aaf54db3b1b4cee38ddabce5896b33eb693275f470"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
 [[package]]
 name = "either"
 version = "1.8.0"
@@ -171,6 +261,30 @@ dependencies = [
  "libc",
 ]
 
+[[package]]
+name = "iana-time-zone"
+version = "0.1.53"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "64c122667b287044802d6ce17ee2ddf13207ed924c712de9a66a5814d5b64765"
+dependencies = [
+ "android_system_properties",
+ "core-foundation-sys",
+ "iana-time-zone-haiku",
+ "js-sys",
+ "wasm-bindgen",
+ "winapi",
+]
+
+[[package]]
+name = "iana-time-zone-haiku"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0703ae284fc167426161c2e3f1da3ea71d94b21bedbcc9494e92b28e334e3dca"
+dependencies = [
+ "cxx",
+ "cxx-build",
+]
+
 [[package]]
 name = "indexmap"
 version = "1.9.2"
@@ -196,6 +310,15 @@ version = "1.0.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "4217ad341ebadf8d8e724e264f13e593e0648f5b3e94b3896a5df283be015ecc"
 
+[[package]]
+name = "js-sys"
+version = "0.3.60"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "49409df3e3bf0856b916e2ceaca09ee28e6871cf7d9ce97a692cacfdb2a25a47"
+dependencies = [
+ "wasm-bindgen",
+]
+
 [[package]]
 name = "lazy_static"
 version = "1.4.0"
@@ -221,6 +344,24 @@ version = "0.2.137"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "fc7fcc620a3bff7cdd7a365be3376c97191aeaccc2a603e600951e452615bf89"
 
+[[package]]
+name = "link-cplusplus"
+version = "1.0.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9272ab7b96c9046fbc5bc56c06c117cb639fe2d509df0c421cad82d2915cf369"
+dependencies = [
+ "cc",
+]
+
+[[package]]
+name = "log"
+version = "0.4.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e"
+dependencies = [
+ "cfg-if",
+]
+
 [[package]]
 name = "memchr"
 version = "2.5.0"
@@ -247,6 +388,25 @@ dependencies = [
  "version_check",
 ]
 
+[[package]]
+name = "num-integer"
+version = "0.1.45"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9"
+dependencies = [
+ "autocfg",
+ "num-traits",
+]
+
+[[package]]
+name = "num-traits"
+version = "0.2.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd"
+dependencies = [
+ "autocfg",
+]
+
 [[package]]
 name = "object"
 version = "0.29.0"
@@ -256,6 +416,12 @@ dependencies = [
  "memchr",
 ]
 
+[[package]]
+name = "once_cell"
+version = "1.16.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "86f0b0d4bf799edbc74508c1e8bf170ff5f41238e5f8225603ca7caaae2b7860"
+
 [[package]]
 name = "proc-macro-error"
 version = "1.0.4"
@@ -310,6 +476,12 @@ version = "1.0.11"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "4501abdff3ae82a1c1b477a17252eb69cee9e66eb915c1abaa4f44d873df9f09"
 
+[[package]]
+name = "scratch"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9c8132065adcfd6e02db789d9285a0deb2f3fcb04002865ab67d5fb103533898"
+
 [[package]]
 name = "serde"
 version = "1.0.148"
@@ -386,6 +558,15 @@ dependencies = [
  "unicode-xid",
 ]
 
+[[package]]
+name = "termcolor"
+version = "1.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bab24d30b911b2376f3a13cc2cd443142f0c81dda04c118693e35b3835757755"
+dependencies = [
+ "winapi-util",
+]
+
 [[package]]
 name = "textwrap"
 version = "0.11.0"
@@ -395,10 +576,22 @@ dependencies = [
  "unicode-width",
 ]
 
+[[package]]
+name = "time"
+version = "0.1.45"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1b797afad3f312d1c66a56d11d0316f916356d11bd158fbc6ca6389ff6bf805a"
+dependencies = [
+ "libc",
+ "wasi",
+ "winapi",
+]
+
 [[package]]
 name = "toml-cli"
 version = "0.2.0"
 dependencies = [
+ "chrono",
  "failure",
  "nom",
  "serde",
@@ -461,6 +654,66 @@ version = "0.9.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
 
+[[package]]
+name = "wasi"
+version = "0.10.0+wasi-snapshot-preview1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f"
+
+[[package]]
+name = "wasm-bindgen"
+version = "0.2.83"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eaf9f5aceeec8be17c128b2e93e031fb8a4d469bb9c4ae2d7dc1888b26887268"
+dependencies = [
+ "cfg-if",
+ "wasm-bindgen-macro",
+]
+
+[[package]]
+name = "wasm-bindgen-backend"
+version = "0.2.83"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4c8ffb332579b0557b52d268b91feab8df3615f265d5270fec2a8c95b17c1142"
+dependencies = [
+ "bumpalo",
+ "log",
+ "once_cell",
+ "proc-macro2",
+ "quote",
+ "syn",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-macro"
+version = "0.2.83"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "052be0f94026e6cbc75cdefc9bae13fd6052cdcaf532fa6c45e7ae33a1e6c810"
+dependencies = [
+ "quote",
+ "wasm-bindgen-macro-support",
+]
+
+[[package]]
+name = "wasm-bindgen-macro-support"
+version = "0.2.83"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "07bc0c051dc5f23e307b13285f9d75df86bfdf816c5721e573dec1f9b8aa193c"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+ "wasm-bindgen-backend",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-shared"
+version = "0.2.83"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1c38c045535d93ec4f0b4defec448e4291638ee608530863b1e2ba115d4fff7f"
+
 [[package]]
 name = "winapi"
 version = "0.3.9"
@@ -477,6 +730,15 @@ version = "0.4.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
 
+[[package]]
+name = "winapi-util"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178"
+dependencies = [
+ "winapi",
+]
+
 [[package]]
 name = "winapi-x86_64-pc-windows-gnu"
 version = "0.4.0"
diff --git a/Cargo.toml b/Cargo.toml
index cbd9d32..5ca8ecc 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -24,3 +24,4 @@ serde = "1.0"
 serde_json = "1.0"
 structopt = "0.3"
 toml_edit = "0.15"
+chrono = "0.4"
diff --git a/README.md b/README.md
index 297fe6b..e07bc18 100644
--- a/README.md
+++ b/README.md
@@ -103,7 +103,7 @@ SUBCOMMANDS:
     check    Check if a key exists
     get      Print some data from the file
     help     Prints this message or the help of the given subcommand(s)
-    set      Edit the file to set some data (currently, just print modified version)
+    set      Edit the file to set some data
 ```
 
 ### `toml check`
@@ -164,14 +164,16 @@ ARGS:
 ```
 $ toml set --help
 toml-set 0.2.0
-Edit the file to set some data (currently, just print modified version)
+Edit the file to set some data
 
 USAGE:
     toml set <path> <query> <value-str>
 
 FLAGS:
-    -h, --help       Prints help information
-    -V, --version    Prints version information
+        --backup       Create a backup file when `overwrite` is set(default: doesn't create a backup file)
+    -h, --help         Prints help information
+        --overwrite    Overwrite the TOML file (default: print to stdout)
+    -V, --version      Prints version information
 
 ARGS:
     <path>         Path to the TOML file to read
diff --git a/src/main.rs b/src/main.rs
index dff67f0..4b37af6 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,6 +1,9 @@
 mod query_parser;
 
+use chrono::{DateTime, Utc};
 use std::fs;
+use std::fs::OpenOptions;
+use std::io::Write;
 use std::path::PathBuf;
 use std::str;
 
@@ -34,7 +37,7 @@ enum Args {
         #[structopt(flatten)]
         opts: GetOpts,
     },
-    /// Edit the file to set some data (currently, just print modified version)
+    /// Edit the file to set some data
     Set {
         /// Path to the TOML file to read
         #[structopt(parse(from_os_str))]
@@ -43,6 +46,8 @@ enum Args {
         query: String,
         /// String value to place at the given spot (bool, array, etc. are TODO)
         value_str: String, // TODO more forms
+        #[structopt(flatten)]
+        opts: SetOpts,
     },
     // TODO: append/add (name TBD)
 }
@@ -54,6 +59,16 @@ struct GetOpts {
     output_toml: bool,
 }
 
+#[derive(StructOpt)]
+struct SetOpts {
+    /// Overwrite the TOML file (default: print to stdout)
+    #[structopt(long)]
+    overwrite: bool,
+    /// Create a backup file when `overwrite` is set(default: doesn't create a backup file)
+    #[structopt(long)]
+    backup: bool,
+}
+
 #[derive(Debug, Fail)]
 enum CliError {
     #[fail(display = "bad query")]
@@ -73,7 +88,8 @@ fn main() -> Result<(), Error> {
             path,
             query,
             value_str,
-        } => set(path, &query, &value_str)?,
+            opts,
+        } => set(path, &query, &value_str, opts)?,
     }
     Ok(())
 }
@@ -177,9 +193,9 @@ fn print_toml_fragment(doc: &Document, tpath: &[TpathSegment]) {
     print!("{}", doc);
 }
 
-fn set(path: PathBuf, query: &str, value_str: &str) -> Result<(), Error> {
+fn set(path: PathBuf, query: &str, value_str: &str, opts: SetOpts) -> Result<(), Error> {
     let tpath = parse_query_cli(query)?.0;
-    let mut doc = read_parse(path)?;
+    let mut doc = read_parse(path.clone())?;
 
     let mut item = doc.as_item_mut();
     let mut already_inline = false;
@@ -222,8 +238,19 @@ fn set(path: PathBuf, query: &str, value_str: &str) -> Result<(), Error> {
     }
     *item = detect_value(value_str);
 
-    // TODO actually write back
-    print!("{}", doc);
+    if opts.overwrite {
+        // write content to path
+        if opts.backup {
+            let now: DateTime<Utc> = Utc::now();
+            let ext = now.format("%Y%m%d-%H%M%S-%f");
+            let backup_file = format!("{}.{}", path.display(), ext);
+            fs::copy(path.clone(), backup_file)?;
+        }
+        let mut output = OpenOptions::new().write(true).truncate(true).open(path)?;
+        write!(output, "{}", doc)?;
+    } else {
+        print!("{}", doc);
+    }
     Ok(())
 }
 

From be95a30f676f7300a849238bf7b6160fcfe17f7a Mon Sep 17 00:00:00 2001
From: bin liu <liubin0329@gmail.com>
Date: Fri, 2 Dec 2022 20:00:04 +0800
Subject: [PATCH 7/7] add some unit tests and a workflow to run tests in pull
 requests.

Signed-off-by: bin liu <liubin0329@gmail.com>
---
 .github/workflows/ci.yml |  22 +++++
 Cargo.lock               |  51 +++++++++++
 Cargo.toml               |   3 +
 Makefile                 |  24 +++++
 src/main.rs              | 186 ++++++++++++++++++++++++++++++++++++---
 test/test.rs             |   2 +-
 6 files changed, 274 insertions(+), 14 deletions(-)
 create mode 100644 .github/workflows/ci.yml
 create mode 100644 Makefile

diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 0000000..9dc281b
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,22 @@
+name: CI
+on:
+  pull_request:
+    types:
+      - opened
+      - edited
+      - reopened
+      - synchronize
+
+env:
+  CARGO_TERM_COLOR: always
+  RUST_TARGET: x86_64-unknown-linux-musl
+
+jobs:
+  build:
+    runs-on: ubuntu-latest
+    steps:
+    - uses: actions/checkout@v2
+    - name: Build and test
+      uses: gmiam/rust-musl-action@master
+      with:
+        args: make build && make ut
diff --git a/Cargo.lock b/Cargo.lock
index 77638a6..fa77d0b 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -231,6 +231,15 @@ dependencies = [
  "synstructure",
 ]
 
+[[package]]
+name = "fastrand"
+version = "1.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a7a407cfaa3385c4ae6b23e84623d48c2798d06e3e6a1878f7f59f17b3f86499"
+dependencies = [
+ "instant",
+]
+
 [[package]]
 name = "gimli"
 version = "0.26.2"
@@ -295,6 +304,15 @@ dependencies = [
  "hashbrown",
 ]
 
+[[package]]
+name = "instant"
+version = "0.1.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c"
+dependencies = [
+ "cfg-if",
+]
+
 [[package]]
 name = "itertools"
 version = "0.10.5"
@@ -464,6 +482,24 @@ dependencies = [
  "proc-macro2",
 ]
 
+[[package]]
+name = "redox_syscall"
+version = "0.2.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a"
+dependencies = [
+ "bitflags",
+]
+
+[[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 = "rustc-demangle"
 version = "0.1.21"
@@ -558,6 +594,20 @@ dependencies = [
  "unicode-xid",
 ]
 
+[[package]]
+name = "tempfile"
+version = "3.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5cdb1ef4eaeeaddc8fbd371e5017057064af0911902ef36b39801f67cc6d79e4"
+dependencies = [
+ "cfg-if",
+ "fastrand",
+ "libc",
+ "redox_syscall",
+ "remove_dir_all",
+ "winapi",
+]
+
 [[package]]
 name = "termcolor"
 version = "1.1.3"
@@ -597,6 +647,7 @@ dependencies = [
  "serde",
  "serde_json",
  "structopt",
+ "tempfile",
  "toml_edit",
 ]
 
diff --git a/Cargo.toml b/Cargo.toml
index 5ca8ecc..2f232cf 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -25,3 +25,6 @@ serde_json = "1.0"
 structopt = "0.3"
 toml_edit = "0.15"
 chrono = "0.4"
+
+[dev-dependencies]
+tempfile = "3.3.0"
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..3aaba14
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,24 @@
+default: build
+
+CARGO ?= $(shell which cargo)
+RUST_TARGET ?= x86_64-unknown-linux-musl
+
+.format:
+	${CARGO} fmt -- --check
+
+build: .format
+	${CARGO} build --target ${RUST_TARGET} --release
+	# Cargo will skip checking if it is already checked
+	${CARGO} clippy --bins --tests -- -Dwarnings
+
+clean:
+	${CARGO} clean
+
+ut:
+	RUST_BACKTRACE=1 ${CARGO} test --workspace -- --skip integration --nocapture
+
+integration:
+	# run tests under `test` directory
+	RUST_BACKTRACE=1 ${CARGO} test --workspace -- integration --nocapture
+
+test: ut integration
diff --git a/src/main.rs b/src/main.rs
index 4b37af6..94149a3 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -52,14 +52,14 @@ enum Args {
     // TODO: append/add (name TBD)
 }
 
-#[derive(StructOpt)]
+#[derive(Clone, Copy, Default, StructOpt)]
 struct GetOpts {
     /// Print as a TOML fragment (default: print as JSON)
     #[structopt(long)]
     output_toml: bool,
 }
 
-#[derive(StructOpt)]
+#[derive(Clone, Copy, Default, StructOpt)]
 struct SetOpts {
     /// Overwrite the TOML file (default: print to stdout)
     #[structopt(long)]
@@ -137,20 +137,31 @@ fn check(path: PathBuf, query: &str) {
 }
 
 fn get(path: PathBuf, query: &str, opts: GetOpts) -> Result<(), Error> {
+    let value = get_value(path, query, opts)?;
+    if opts.output_toml {
+        print!("{}", value);
+    } else {
+        println!("{}", value);
+    }
+    Ok(())
+}
+
+fn get_value(path: PathBuf, query: &str, opts: GetOpts) -> Result<String, Error> {
     let tpath = parse_query_cli(query)?.0;
     let doc = read_parse(path)?;
 
-    if opts.output_toml {
-        print_toml_fragment(&doc, &tpath);
+    let value = if opts.output_toml {
+        format_toml_fragment(&doc, &tpath)
     } else {
         let item = walk_tpath(doc.as_item(), &tpath);
         // TODO: support shell-friendly output like `jq -r`
-        println!("{}", serde_json::to_string(&JsonItem(item))?);
-    }
-    Ok(())
+        serde_json::to_string(&JsonItem(item))?
+    };
+
+    Ok(value)
 }
 
-fn print_toml_fragment(doc: &Document, tpath: &[TpathSegment]) {
+fn format_toml_fragment(doc: &Document, tpath: &[TpathSegment]) -> String {
     let mut item = doc.as_item();
     let mut breadcrumbs = vec![];
     for seg in tpath {
@@ -190,10 +201,23 @@ fn print_toml_fragment(doc: &Document, tpath: &[TpathSegment]) {
         }
     }
     let doc = Document::from(item.into_table().unwrap());
-    print!("{}", doc);
+    format!("{}", doc)
 }
 
 fn set(path: PathBuf, query: &str, value_str: &str, opts: SetOpts) -> Result<(), Error> {
+    let result = set_value(path, query, value_str, opts)?;
+    if let Some(doc) = result {
+        print!("{}", doc);
+    }
+    Ok(())
+}
+
+fn set_value(
+    path: PathBuf,
+    query: &str,
+    value_str: &str,
+    opts: SetOpts,
+) -> Result<Option<String>, Error> {
     let tpath = parse_query_cli(query)?.0;
     let mut doc = read_parse(path.clone())?;
 
@@ -238,7 +262,7 @@ fn set(path: PathBuf, query: &str, value_str: &str, opts: SetOpts) -> Result<(),
     }
     *item = detect_value(value_str);
 
-    if opts.overwrite {
+    let result = if opts.overwrite {
         // write content to path
         if opts.backup {
             let now: DateTime<Utc> = Utc::now();
@@ -248,10 +272,12 @@ fn set(path: PathBuf, query: &str, value_str: &str, opts: SetOpts) -> Result<(),
         }
         let mut output = OpenOptions::new().write(true).truncate(true).open(path)?;
         write!(output, "{}", doc)?;
+        None
     } else {
-        print!("{}", doc);
-    }
-    Ok(())
+        Some(format!("{}", doc))
+    };
+
+    Ok(result)
 }
 
 fn detect_value(value_str: &str) -> Item {
@@ -353,3 +379,137 @@ impl Serialize for JsonValue<'_> {
         }
     }
 }
+
+#[cfg(test)]
+mod tests {
+    use std::fs;
+
+    // functions to test
+    use super::check_exists;
+    use super::detect_value;
+    use super::{get_value, GetOpts};
+    use super::{set_value, SetOpts};
+
+    #[test]
+    fn test_detect_value() {
+        let i = detect_value("abc");
+        assert_eq!("string", i.type_name());
+        assert!(i.is_str());
+        assert_eq!(Some("abc"), i.as_str());
+
+        let i = detect_value("123");
+        assert_eq!("integer", i.type_name());
+        assert!(i.is_integer());
+        assert_eq!(Some(123), i.as_integer());
+
+        let i = detect_value("true");
+        assert_eq!("boolean", i.type_name());
+        assert!(i.is_bool());
+        assert_eq!(Some(true), i.as_bool());
+    }
+
+    #[test]
+    fn test_check_exists() {
+        let body = r#"[a]
+b = "c"
+[x]
+y = "z""#;
+        let dir = tempfile::tempdir().expect("failed to create tempdir");
+        let toml_file = dir.path().join("test.toml");
+        fs::write(&toml_file, body).expect("failed to create tempfile");
+
+        // x.y exists
+        let result = check_exists(toml_file.clone(), "x.y");
+        assert!(result.is_ok());
+        assert!(result.unwrap());
+
+        // x.z does not exists
+        let result = check_exists(toml_file, "x.z");
+        assert!(result.is_ok());
+        assert!(!result.unwrap());
+    }
+
+    #[test]
+    fn test_get_value() {
+        let body = r#"[a]
+b = "c"
+[x]
+y = "z""#;
+        let dir = tempfile::tempdir().expect("failed to create tempdir");
+        let toml_file = dir.path().join("test.toml");
+        fs::write(&toml_file, body).expect("failed to write tempfile");
+
+        let opts = GetOpts::default();
+        // x.y exists
+        let result = get_value(toml_file.clone(), "x.y", opts);
+        assert!(result.is_ok());
+        assert_eq!("\"z\"", result.unwrap());
+
+        // x.z does not exists
+        // FIXME: get_value now will panic, it's not a well-desined API.
+        let result = std::panic::catch_unwind(|| {
+            let _ = get_value(toml_file.clone(), "x.z", opts);
+        });
+        assert!(result.is_err());
+    }
+
+    #[test]
+    fn test_set_value() {
+        // fn set(path: PathBuf, query: &str, value_str: &str, opts: SetOpts) -> Result<(), Error> {
+        let body = r#"[a]
+b = "c"
+[x]
+y = "z""#;
+        let dir = tempfile::tempdir().expect("failed to create tempdir");
+        let toml_file = dir.path().join("test.toml");
+        fs::write(&toml_file, body).expect("failed to write tempfile");
+
+        let mut opts = SetOpts::default();
+        // x.y exists
+        let result = set_value(toml_file.clone(), "x.y", "new", opts);
+        assert!(result.is_ok());
+        let excepted = r#"[a]
+b = "c"
+[x]
+y = "new"
+"#;
+        assert_eq!(excepted, result.unwrap().unwrap());
+
+        let result = set_value(toml_file.clone(), "x.z", "123", opts);
+        assert!(result.is_ok());
+        let excepted = r#"[a]
+b = "c"
+[x]
+y = "z"
+z = 123
+"#;
+        assert_eq!(excepted, result.unwrap().unwrap());
+
+        let result = set_value(toml_file.clone(), "x.z", "false", opts);
+        assert!(result.is_ok());
+        let excepted = r#"[a]
+b = "c"
+[x]
+y = "z"
+z = false
+"#;
+        assert_eq!(excepted, result.unwrap().unwrap());
+
+        // test overwrite the original file
+        opts.overwrite = true;
+        let result = set_value(toml_file.clone(), "x.z", "false", opts);
+        assert!(result.is_ok());
+        println!("{:?}", result);
+        // --overwrite will not generate any output.
+        assert_eq!(None, result.unwrap());
+
+        let excepted = r#"[a]
+b = "c"
+[x]
+y = "z"
+z = false
+"#;
+        let new_body = fs::read_to_string(toml_file).expect("failed to read TOML file");
+        assert_eq!(excepted, new_body);
+    }
+}
diff --git a/test/test.rs b/test/test.rs
index f17eb54..d11b7b6 100644
--- a/test/test.rs
+++ b/test/test.rs
@@ -5,7 +5,7 @@ use std::process;
 use std::str;
 
 #[test]
-fn help_if_no_args() {
+fn integration_test_help_if_no_args() {
     // Probably want to factor out much of this when adding more tests.
     let proc = process::Command::new(get_exec_path()).output().unwrap();
     assert!(!proc.status.success());