Skip to content

Commit 680b0d4

Browse files
committed
v1.3.0
feat: add variable substitution, export, and import - Add SINBO:name: placeholder syntax with --args flag on get - Add export command to .sinbo.json format - Add import command from .sinbo.json - Prompt on name conflict during export/import - Update README with vars, export/import docs and examples
1 parent 716b34a commit 680b0d4

8 files changed

Lines changed: 270 additions & 21 deletions

File tree

CHANGELOG.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,15 @@ All notable changes to sinbo will be documented here.
44

55
---
66

7+
## 1.3.0 - 2026-04-12
8+
9+
### Added
10+
- Variable substitution system with `SINBO:name:` placeholder syntax
11+
- `--args key=value` flag on `get` for placeholder substitution
12+
- `export` command — export snippets to `.sinbo.json` files
13+
- `import` command — import snippets from `.sinbo.json` files
14+
- Conflict resolution prompt on import/export name collision
15+
716
## 1.2.1 - 2026-04-11
817

918
### Added

CONTRIBUTING.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Contributing
22

3-
Contributions are welcome bug fixes, new features, or doc improvements.
3+
Contributions are welcome, bug fixes, new features, or doc improvements.
44

55
If you're planning something large, [open an issue](https://github.com/opmr0/sinbo/issues) first so we can discuss it before you invest the time.
66

Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "sinbo"
3-
version = "1.2.1"
3+
version = "1.3.0"
44
edition = "2024"
55
description = "A CLI snippet manager. Store code once, retrieve it anywhere."
66
license = "MIT"

readme.md

Lines changed: 43 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
<div align=right>Table of Contents↗️</div>
2+
13
<img src="./assets/logo_no_bg.png" alt="sinbo logo" width="100"/>
24

35
<br>
@@ -81,6 +83,7 @@ sinbo add rust-test -e rs # open editor with .rs syntax
8183
sinbo add center-div -f style.css # read from file
8284
sinbo add docker-run -t docker infra # add with tags
8385
sinbo add api-key --encrypt # encrypt with a password
86+
sinbo get docker-run --args port=8080 name=myapp # substitute placeholders
8487
echo "hello world" | sinbo add greeting # read from stdin
8588
```
8689

@@ -90,6 +93,7 @@ echo "hello world" | sinbo add greeting # read from stdin
9093
| `--file-name` | `-f` | Read content from a file |
9194
| `--tags` | `-t` | Tag the snippet |
9295
| `--description` | `-d` | Add a description to the snippet |
96+
| `--args` | | Substitute placeholders (`key=value`) |
9397
| `--encrypt` | | Encrypt the snippet with a password |
9498

9599
---
@@ -221,6 +225,39 @@ Encryption uses AES-256-GCM with Argon2id key derivation. The plaintext never to
221225
222226
---
223227

228+
## Variables
229+
230+
Snippets can contain placeholders using the `SINBO:name:` syntax.
231+
232+
```bash
233+
# snippet content:
234+
docker run -p SINBO:port: -it SINBO:name:
235+
236+
# usage:
237+
sinbo get docker-run --args port=8080 name=myapp
238+
# output: docker run -p 8080 -it myapp
239+
```
240+
241+
If a placeholder has no matching `--args` value, it is left as-is in the output.
242+
243+
---
244+
245+
## Export / Import
246+
247+
Snippets can be exported to `.sinbo.json` files and imported back.
248+
249+
```bash
250+
sinbo export docker-run # export to current directory
251+
sinbo export docker-run -p ~/backups # export to a specific directory
252+
sinbo import ~/backups/docker-run.sinbo.json # import from file
253+
```
254+
255+
Encrypted snippets cannot be exported, decrypt them first.
256+
257+
If a name conflict is detected on import or export, sinbo will prompt you to overwrite or rename.
258+
259+
---
260+
224261
## How It Works
225262

226263
Snippets are stored as plain files in your system config directory:
@@ -230,11 +267,12 @@ Snippets are stored as plain files in your system config directory:
230267

231268
Each snippet consists of up to two files:
232269

233-
| File | Contents |
234-
| ------------------ | ------------------------------ |
235-
| `{name}.code` | Plaintext snippet content |
236-
| `{name}.enc` | Encrypted snippet content |
237-
| `{name}.meta.json` | Tags, extension, and timestamp |
270+
| File | Contents |
271+
| ------------------- | ------------------------------ |
272+
| `{name}.code` | Plaintext snippet content |
273+
| `{name}.enc` | Encrypted snippet content |
274+
| `{name}.meta.json` | Tags, extension, and timestamp |
275+
| `{name}.sinbo.json` | Exported snippet file |
238276

239277
Plain `.code` files are grep-able, copyable, and easy to back up directly.
240278

src/main.rs

Lines changed: 48 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
use std::{
22
env, fs,
33
io::{self, IsTerminal, Read},
4+
path::PathBuf,
45
process::Command,
56
};
67

@@ -13,6 +14,8 @@ use fuzzy_matcher::skim::SkimMatcherV2;
1314

1415
mod encryption;
1516
mod storage;
17+
mod transfer;
18+
mod var;
1619

1720
use crate::storage::SnippetMeta;
1821
use storage::Storage;
@@ -31,54 +34,68 @@ enum Action {
3134
name: String,
3235
#[arg(short, long, help = "Copy to clipboard instead of printing")]
3336
copy: bool,
37+
#[arg(long, value_parser = parse_key_val, help = "Substitute placeholders (key=value)")]
38+
args: Vec<(String, String)>,
3439
},
3540
#[command(about = "Add a new snippet", alias = "a")]
3641
Add {
3742
name: String,
3843
#[arg(long, short, num_args = 1, help = "Read content from a file")]
3944
file_name: Option<String>,
40-
#[arg(short, long, num_args = 1.., help = "Tags for the snippet")]
45+
#[arg(short, long, num_args = 1.., help = "Assign tags to the snippet")]
4146
tags: Option<Vec<String>>,
4247
#[arg(
4348
short,
4449
long,
4550
num_args = 1,
46-
help = "File extension for syntax highlighting in editor"
51+
help = "File extension for editor syntax highlighting"
4752
)]
4853
ext: Option<String>,
49-
#[arg(long, help = "Encrypt the snippet (prompted for password)")]
54+
#[arg(long, help = "Encrypt the snippet with a password")]
5055
encrypt: bool,
51-
#[arg(long, short, num_args = 1, help = "Add a description to metadata")]
56+
#[arg(long, short, num_args = 1, help = "Short description for the snippet")]
5257
description: Option<String>,
5358
},
5459
#[command(about = "List all snippets", alias = "l")]
5560
List {
56-
#[arg(short, long, num_args = 1.., help = "Filter by tags")]
61+
#[arg(short, long, num_args = 1.., help = "Filter snippets by tag")]
5762
tags: Option<Vec<String>>,
58-
#[arg(short, long, help = "Show the snippets content")]
63+
#[arg(short, long, help = "Show snippet content")]
5964
show: bool,
6065
},
6166
#[command(about = "Remove a snippet", alias = "r")]
6267
Remove { name: String },
6368
#[command(about = "Edit an existing snippet", alias = "e")]
6469
Edit {
6570
name: String,
66-
#[arg(short, long, num_args = 1.., help = "Update tags")]
71+
#[arg(short, long, num_args = 1.., help = "Replace the snippet's tags")]
6772
tags: Option<Vec<String>>,
68-
#[arg(long, short, num_args = 1, help = "Add a description to metadata")]
73+
#[arg(long, short, num_args = 1, help = "Update the snippet's description")]
6974
description: Option<String>,
7075
},
71-
#[command(about = "Search a query in snippets", alias = "s")]
76+
#[command(about = "Search snippets by name or content", alias = "s")]
7277
Search {
73-
#[arg(short, long, num_args = 1.., help = "search in tags")]
78+
#[arg(short, long, num_args = 1.., help = "Narrow search to specific tags")]
7479
tags: Option<Vec<String>>,
7580
query: String,
7681
},
7782
#[command(about = "Encrypt an existing snippet")]
7883
Encrypt { name: String },
79-
8084
#[command(about = "Decrypt a snippet permanently")]
8185
Decrypt { name: String },
86+
#[command(about = "Export a snippet to a .sinbo.json file")]
87+
Export {
88+
name: String,
89+
#[arg(
90+
short,
91+
long,
92+
num_args = 1,
93+
help = "Directory to export into (default: current dir)"
94+
)]
95+
path: Option<PathBuf>,
96+
},
97+
#[command(about = "Import a snippet from a .sinbo.json file")]
98+
Import { path: std::path::PathBuf },
8299
}
83100

84101
fn confirm() {
@@ -144,10 +161,10 @@ fn main() -> Result<()> {
144161
let storage = Storage::new();
145162

146163
match args.action {
147-
Action::Get { name, copy } => {
164+
Action::Get { name, copy, args } => {
148165
let snippet = storage.get(&name)?;
149166

150-
let content = if snippet.encrypted {
167+
let mut content = if snippet.encrypted {
151168
let password = encryption::prompt_password("Password: ")?;
152169
let enc_path = storage.snippet_path(&name).with_extension("enc");
153170
let bytes = encryption::read_encrypted(&enc_path, password.as_bytes())
@@ -157,6 +174,11 @@ fn main() -> Result<()> {
157174
snippet.content
158175
};
159176

177+
if !args.is_empty() {
178+
let map: std::collections::HashMap<String, String> = args.into_iter().collect();
179+
content = var::substitute(&content, &map)?;
180+
}
181+
160182
if copy {
161183
let mut clipboard = arboard::Clipboard::new()?;
162184
clipboard.set_text(&content)?;
@@ -449,6 +471,13 @@ fn main() -> Result<()> {
449471

450472
eprintln!("{} decrypted '{}'", "sinbo".cyan().bold(), name.yellow());
451473
}
474+
Action::Export { name, path } => {
475+
let snippet = storage.get(&name)?;
476+
transfer::export(&snippet, path)?;
477+
}
478+
Action::Import { path } => {
479+
transfer::import(path, storage)?;
480+
}
452481
}
453482

454483
Ok(())
@@ -460,3 +489,9 @@ fn now_secs() -> u64 {
460489
.unwrap()
461490
.as_secs()
462491
}
492+
493+
fn parse_key_val(s: &str) -> Result<(String, String), String> {
494+
s.split_once('=')
495+
.map(|(k, v)| (k.to_string(), v.to_string()))
496+
.ok_or_else(|| format!("invalid key=value pair: '{}'", s))
497+
}

0 commit comments

Comments
 (0)