1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
use std::path::PathBuf;
use clap::{Parser, Subcommand, ValueEnum};
#[derive(Parser)]
#[command(name = "obsidian", about = "Query and navigate Obsidian vaults")]
pub struct Cli {
/// Path to the vault directory. Defaults to the nearest parent directory containing
/// '.obsidian/', or the current directory if none is found.
#[arg(
long,
short = 'v',
global = true,
env = "OBSIDIAN_VAULT",
help_heading = "Global options"
)]
pub vault: Option<PathBuf>,
/// Force color output even when not writing to a TTY
#[arg(long, global = true, help_heading = "Global options")]
pub color: bool,
/// Disable color output
#[arg(long, global = true, help_heading = "Global options")]
pub no_color: bool,
#[command(subcommand)]
pub command: Command,
}
#[derive(Subcommand)]
pub enum Command {
/// Search for notes in the vault.
///
/// Filters are applied in two stages:
///
/// 1. First 'AND' filters are applied to narrow down the candidate set of notes.
/// If any 'AND' filter is specified, only notes that match all 'AND' filters will be included
/// in the candidate set. If no 'AND' filters are specified, all notes in the vault will be
/// included in the candidate set.
///
/// 2. Then 'OR' filters are applied to the candidate set. If any 'OR' filter is specified, only notes
/// that match at least one 'OR' filter will be included in the final results. If no 'OR' filters
/// are specified, all notes in the candidate set will be included in the final results.
Search(Box<SearchArgs>),
/// Work with individual notes
Note(NoteArgs),
/// Work with tags across the vault
Tags(TagsArgs),
/// Check vault health: report duplicate IDs/aliases and broken links
Check(CheckArgs),
}
#[derive(clap::Args)]
pub struct CheckArgs {
/// Ignore notes matching this glob pattern (matched against vault-relative path, repeatable)
#[arg(long, short = 'i')]
pub ignore: Vec<String>,
}
#[derive(clap::Args)]
pub struct SearchArgs {
/// Only include notes whose path matches one of these glob patterns (matched against vault-relative path, repeatable)
#[arg(long, help_heading = "Path filters")]
pub glob: Vec<String>,
/// Same as --glob but with global OR semantics
#[arg(long, help_heading = "Path filters")]
pub or_glob: Vec<String>,
/// Filter by exact note ID match (AND semantics)
#[arg(long, help_heading = "Metadata filters")]
pub id: Option<String>,
/// Filter by exact note ID match, case-sensitive by default (OR semantics, repeatable)
#[arg(long, help_heading = "Metadata filters")]
pub or_id: Vec<String>,
/// Filter by tag, case-insensitive by default (AND semantics, repeatable)
#[arg(long, help_heading = "Metadata filters")]
pub tag: Vec<String>,
/// Filter by tag, case-insensitive by default (OR semantics, repeatable)
#[arg(long, help_heading = "Metadata filters")]
pub or_tag: Vec<String>,
/// Filter by title substring, smart case-sensitive by default (AND semantics, repeatable)
#[arg(long, help_heading = "Metadata filters")]
pub title_contains: Vec<String>,
/// Filter by title substring, smart case-sensitive by default (OR semantics, repeatable)
#[arg(long, help_heading = "Metadata filters")]
pub or_title_contains: Vec<String>,
/// Filter by exact alias, smart case-sensitive by default (AND semantics, repeatable)
#[arg(long, help_heading = "Metadata filters")]
pub alias: Vec<String>,
/// Filter by exact alias, smart case-sensitive by default (OR semantics, repeatable)
#[arg(long, help_heading = "Metadata filters")]
pub or_alias: Vec<String>,
/// Filter by alias substring, smart case-sensitive by default (AND semantics, repeatable)
#[arg(long, help_heading = "Metadata filters")]
pub alias_contains: Vec<String>,
/// Filter by alias substring, smart case-sensitive by default (OR semantics, repeatable)
#[arg(long, help_heading = "Metadata filters")]
pub or_alias_contains: Vec<String>,
/// Filter by content substring, smart case-sensitive by default (AND semantics, repeatable)
#[arg(long, help_heading = "Content filters")]
pub content_contains: Vec<String>,
/// Filter by content substring, smart case-sensitive by default (OR semantics, repeatable)
#[arg(long, help_heading = "Content filters")]
pub or_content_contains: Vec<String>,
/// Filter by content pattern, smart case-sensitive by default (AND semantics, repeatable).
/// See https://docs.rs/regex/latest/regex/#syntax.
#[arg(long, help_heading = "Content filters")]
pub content_matches: Vec<String>,
/// Filter by content pattern, smart case-sensitive by default (OR semantics, repeatable).
/// See https://docs.rs/regex/latest/regex/#syntax.
#[arg(long, help_heading = "Content filters")]
pub or_content_matches: Vec<String>,
/// Execute the search case sensitive. By default, title, alias, and content filters are
/// smart case-sensitive, while ID filters are case-sensitive, and tag filters are case-insensitive.
/// This flag overrides -i/--ignore-case and -S/--smart-case.
#[arg(long, short = 's', help_heading = "Filter behavior")]
pub case_sensitive: bool,
/// Execute the search case insensitive. This flag overrides -S/--smart-case.
#[arg(long, short = 'i', help_heading = "Filter behavior")]
pub ignore_case: bool,
/// Search case insensitively for patterns that are all lowercase, otherwise search case
/// sensitively.
#[arg(long, short = 'S', help_heading = "Filter behavior")]
pub smart_case: bool,
/// Include inline tags in the search, as opposed to just frontmatter tags
#[arg(long, help_heading = "Filter behavior")]
pub inline_tags: bool,
/// Sort order for results
#[arg(long, help_heading = "Output options")]
pub sort: Option<SortOrder>,
/// Output format
#[arg(long, short = 'f', default_value = "plain", help_heading = "Output options")]
pub format: OutputFormat,
}
#[derive(clap::Args)]
pub struct BacklinksArgs {
/// Path to the note (resolved relative to current directory)
pub note: PathBuf,
/// Sort order for results
#[arg(long, short = 's', help_heading = "Output options")]
pub sort: Option<SortOrder>,
/// Output format
#[arg(long, short = 'f', default_value = "plain", help_heading = "Output options")]
pub format: OutputFormat,
}
#[derive(clap::Args)]
pub struct RenameArgs {
/// Path to the note to rename (resolved relative to current directory)
pub note: PathBuf,
/// New path for the note (resolved relative to current directory, .md added if omitted)
pub new_path: PathBuf,
/// Preview what would change without modifying any files
#[arg(long)]
pub dry_run: bool,
/// Output format
#[arg(long, short = 'f', default_value = "plain", help_heading = "Output options")]
pub format: OutputFormat,
}
#[derive(clap::Args)]
pub struct MergeArgs {
/// One or more source notes followed by the destination note.
/// All paths are resolved relative to the current directory.
/// The last path is the destination; all preceding paths are sources.
/// Sources are merged into the destination (which is created if it doesn't exist) and deleted.
#[arg(name = "PATH", required = true, num_args = 2..)]
pub paths: Vec<PathBuf>,
/// Preview what would change without modifying any files
#[arg(long)]
pub dry_run: bool,
/// Output format
#[arg(long, short = 'f', default_value = "plain", help_heading = "Output options")]
pub format: OutputFormat,
}
#[derive(clap::Args)]
pub struct NoteArgs {
#[command(subcommand)]
pub subcommand: NoteCommand,
}
#[derive(Subcommand)]
pub enum NoteCommand {
/// Resolve a note from a path, ID, or alias
Resolve(ResolveArgs),
/// List all notes
List(ListArgs),
/// Search for notes (alias for 'obsidian search')
Search(Box<SearchArgs>),
/// Read contents/frontmatter of a note
Read(ReadArgs),
/// Write a new note
Write(WriteArgs),
/// Find notes that link to a given note
Backlinks(BacklinksArgs),
/// Merge two or more notes into a single destination note
Merge(MergeArgs),
/// Patch the content of a note by replacing one exact string with another
Patch(PatchArgs),
/// Rename a note and update all backlinks
Rename(RenameArgs),
/// Update frontmatter metadata fields of a note
Update(UpdateArgs),
}
#[derive(clap::Args)]
pub struct ResolveArgs {
/// Path, ID, or alias of the note to resolve
pub note: String,
/// Output format
#[arg(long, short = 'f', default_value = "plain", help_heading = "Output options")]
pub format: OutputFormat,
}
#[derive(clap::Args)]
pub struct ListArgs {
/// Sort order for results
#[arg(long, short = 's', help_heading = "Output options")]
pub sort: Option<SortOrder>,
/// Output format
#[arg(long, short = 'f', default_value = "plain", help_heading = "Output options")]
pub format: OutputFormat,
}
#[derive(clap::Args)]
pub struct ReadArgs {
/// Path to the note to read (resolved relative to current directory)
pub note: PathBuf,
/// Include frontmatter in the output
#[arg(long, help_heading = "Output options")]
pub frontmatter: bool,
/// Exclude content from the output (--frontmatter is assumed if this is set)
#[arg(long, help_heading = "Output options")]
pub no_content: bool,
/// Output format
#[arg(long, short = 'f', default_value = "plain", help_heading = "Output options")]
pub format: OutputFormat,
}
#[derive(clap::Args)]
pub struct WriteArgs {
/// Path to the note to write (resolved relative to the vault root or current directory, .md added if omitted)
pub note: PathBuf,
/// Content to write to the note. If omitted, content is read from stdin.
pub content: Option<String>,
/// A title for the note if one can't be inferred from the content
#[arg(long, short = 't')]
pub title: Option<String>,
/// Add tag(s) to frontmatter (repeatable)
#[arg(long)]
pub tag: Vec<String>,
/// Add alias(es) to frontmatter (repeatable)
#[arg(long, short = 'a')]
pub alias: Vec<String>,
/// Force overwrite any existing note
#[arg(long)]
pub force: bool,
/// Output format
#[arg(long, short = 'f', default_value = "plain", help_heading = "Output options")]
pub format: OutputFormat,
}
#[derive(clap::Args)]
pub struct PatchArgs {
/// Path to the note (resolved relative to current directory)
pub note: PathBuf,
/// The exact string to find (must appear exactly once in the note)
#[arg(long)]
pub old_string: String,
/// The string to replace it with
#[arg(long)]
pub new_string: String,
/// Output format
#[arg(long, short = 'f', default_value = "plain", help_heading = "Output options")]
pub format: OutputFormat,
}
#[derive(clap::Args)]
pub struct UpdateArgs {
/// Path to the note (resolved relative to vault root or current directory).
/// If omitted, note paths are read from stdin (one per line).
pub note: Option<PathBuf>,
/// Add tag(s) to frontmatter (repeatable)
#[arg(long, short = 't')]
pub add_tag: Vec<String>,
/// Remove tag(s) from frontmatter (repeatable)
#[arg(long)]
pub rm_tag: Vec<String>,
/// Add alias(es) to frontmatter (repeatable)
#[arg(long, short = 'a')]
pub add_alias: Vec<String>,
/// Set a field in the frontmatter to a value (repeatable, --set key=value). The value is
/// parsed as YAML, so it can be a string (with or without quotes), number, boolean, list, map, or null.
/// If the field already exits, it will be overwritten. To remove a field, set it to null (e.g. --set myfield=null).
#[arg(long)]
pub set: Vec<String>,
/// Output format
#[arg(long, short = 'f', default_value = "plain", help_heading = "Output options")]
pub format: OutputFormat,
}
#[derive(Clone, ValueEnum)]
pub enum OutputFormat {
Plain,
Json,
}
#[derive(Clone, ValueEnum)]
pub enum SortOrder {
PathAsc,
PathDesc,
ModifiedAsc,
ModifiedDesc,
CreatedAsc,
CreatedDesc,
}
// Get SortOrder from the corresponding type in obsidian-core.
impl From<SortOrder> for obsidian_core::SortOrder {
fn from(sort_order: SortOrder) -> Self {
match sort_order {
SortOrder::PathAsc => obsidian_core::SortOrder::PathAsc,
SortOrder::PathDesc => obsidian_core::SortOrder::PathDesc,
SortOrder::ModifiedAsc => obsidian_core::SortOrder::ModifiedAsc,
SortOrder::ModifiedDesc => obsidian_core::SortOrder::ModifiedDesc,
SortOrder::CreatedAsc => obsidian_core::SortOrder::CreatedAsc,
SortOrder::CreatedDesc => obsidian_core::SortOrder::CreatedDesc,
}
}
}
#[derive(clap::Args)]
pub struct TagsArgs {
#[command(subcommand)]
pub subcommand: TagsCommand,
}
#[derive(Subcommand)]
pub enum TagsCommand {
/// Find all occurrences of the given tags across the vault
Search(TagsSearchArgs),
/// List all tags used across the vault
List(TagsListArgs),
}
#[derive(clap::Args)]
pub struct TagsSearchArgs {
/// Tags to search for (OR semantics — occurrences of any given tag are shown)
#[arg(required = true)]
pub tags: Vec<String>,
/// Sort order for results
#[arg(long, short = 's', help_heading = "Output options")]
pub sort: Option<SortOrder>,
/// Output format
#[arg(long, short = 'f', default_value = "plain", help_heading = "Output options")]
pub format: OutputFormat,
}
#[derive(clap::Args)]
pub struct TagsListArgs {
/// Output format
#[arg(long, short = 'f', default_value = "plain", help_heading = "Output options")]
pub format: OutputFormat,
}