Skip to content

Commit

Permalink
Add support for control placeholders (#9)
Browse files Browse the repository at this point in the history
- `%R` enable raw mode - render but do not format output
- `%Y` enable YAML mode - format output into YAML
- `%J` enable pretty JSON mode - format output into pretty JSON

Example:

```bash
jf "%R%*q" a b c
"a","b","c"

jf "%Y[%*q]" a b c
- a
- b
- c

jf "%J[%*q]" a b c
[
  "a",
  "b",
  "c"
]
```
  • Loading branch information
sayanarijit committed May 27, 2023
1 parent 6e85ea9 commit cbbd746
Show file tree
Hide file tree
Showing 8 changed files with 94 additions and 20 deletions.
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "jf"
version = "0.4.1"
version = "0.4.2"
edition = "2021"
authors = ["Arijit Basu <hi@arijitbasu.in>"]
description = 'A small utility to safely format and print JSON objects in the commandline'
Expand Down
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,9 @@ Where TEMPLATE may contain the following placeholders:
- `%s` JSON values other than string
- `%v` the `jf` version number
- `%%` a literal `%` character
- `%R` enable raw mode - render but do not format output
- `%Y` enable pretty YAML mode - format output into pretty YAML
- `%J` enable pretty JSON mode - format output into pretty JSON

And [VALUE]... [NAME=VALUE]... [NAME@FILE]... are the values for the placeholders.

Expand Down
12 changes: 12 additions & 0 deletions assets/jf.1
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,18 @@ the `jf` version number
.B
`%%`
a literal `%` character
.TP
.B
`%R`
enable raw mode - render but do not format output
.TP
.B
`%Y`
enable pretty YAML mode - format output into pretty YAML
.TP
.B
`%J`
enable pretty JSON mode - format output into pretty JSON
.PP
And [VALUE]\.\.\. [NAME=VALUE]\.\.\. [NAME@FILE]\.\.\. are the values for the placeholders.
.SH SYNTAX
Expand Down
46 changes: 40 additions & 6 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -436,16 +436,25 @@ where
Ok(was_expanded)
}

pub fn format_partial<'a, C, A, S>(
#[derive(Debug)]
enum Mode {
Json,
Raw,
Yaml,
PrettyJson,
}

fn format_partial<'a, C, A, S>(
chars: &mut C,
args: &mut A,
stdin: &mut S,
) -> Result<(String, Option<char>), Error>
) -> Result<(String, Option<char>, Mode), Error>
where
C: Iterator<Item = (usize, char)>,
A: Iterator<Item = (usize, Cow<'a, str>)>,
S: Iterator<Item = (usize, io::Result<Vec<u8>>)>,
{
let mut mode = Mode::Json;
let mut val = "".to_string();
let mut last_char = None;
let mut is_reading_named_values = false;
Expand All @@ -467,6 +476,18 @@ where
val.push_str(VERSION);
last_char = None;
}
('R', Some('%')) => {
mode = Mode::Raw;
last_char = None;
}
('Y', Some('%')) => {
mode = Mode::Yaml;
last_char = None;
}
('J', Some('%')) => {
mode = Mode::PrettyJson;
last_char = None;
}
('%', _) => {
last_char = Some(ch);
}
Expand Down Expand Up @@ -545,7 +566,7 @@ where
}
}

Ok((val, last_char))
Ok((val, last_char, mode))
}

/// Format a string using the given arguments.
Expand All @@ -561,7 +582,7 @@ where
let mut chars = format.chars().enumerate();
let mut stdin = io::stdin().lock().split(b'\0').enumerate();

let (val, last_char) = format_partial(&mut chars, &mut args, &mut stdin)?;
let (val, last_char, mode) = format_partial(&mut chars, &mut args, &mut stdin)?;

if last_char == Some('%') {
return Err("template ended with incomplete placeholder".into());
Expand All @@ -573,6 +594,19 @@ where
);
};

let val: yaml::Value = yaml::from_str(&val).map_err(Error::from)?;
json::to_string(&val).map_err(Error::from)
match mode {
Mode::Raw => Ok(val),
Mode::Yaml => yaml::from_str::<yaml::Value>(&val)
.and_then(|y| yaml::to_string(&y))
.map_err(Error::from),
Mode::Json => yaml::from_str::<yaml::Value>(&val)
.map_err(Error::from)
.and_then(|y| json::to_string(&y).map_err(Error::from)),
Mode::PrettyJson => yaml::from_str::<yaml::Value>(&val)
.map_err(Error::from)
.and_then(|y| json::to_string_pretty(&y).map_err(Error::from)),
}
}

#[cfg(test)]
mod tests;
3 changes: 0 additions & 3 deletions src/main.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,3 @@
#[cfg(test)]
mod tests;

fn main() {
let args = std::env::args().skip(1).map(Into::into);

Expand Down
43 changes: 34 additions & 9 deletions src/tests.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use crate as jf;
use serde_json as json;
use std::borrow::Cow;
use std::io;
Expand Down Expand Up @@ -36,7 +37,7 @@ fn test_format_from_stdin() {
.into_iter()
.enumerate();

let (res, _) = jf::format_partial(&mut chars, &mut args, &mut stdin).unwrap();
let (res, _, _) = jf::format_partial(&mut chars, &mut args, &mut stdin).unwrap();
assert_eq!(res, r#"{"one": 1, "two": 2, "three": 3}"#);

let mut chars =
Expand All @@ -52,7 +53,7 @@ fn test_format_from_stdin() {

let mut args = ["1", "true", "bar"].map(Cow::from).into_iter().enumerate();

let (res, _) = jf::format_partial(&mut chars, &mut args, &mut stdin).unwrap();
let (res, _, _) = jf::format_partial(&mut chars, &mut args, &mut stdin).unwrap();
assert_eq!(
res,
r#"{"1": 1, one: "1", "true": true, truestr: "true", foo: foo, bar: "bar", esc: "%"}"#
Expand All @@ -71,7 +72,7 @@ fn test_format_expand_items_from_stdin() {

let mut args = ["2", "false", "bar"].map(Cow::from).into_iter().enumerate();

let (res, _) = jf::format_partial(&mut chars, &mut args, &mut stdin).unwrap();
let (res, _, _) = jf::format_partial(&mut chars, &mut args, &mut stdin).unwrap();
assert_eq!(res, r#"[start, 1,true,foo, mid, 2,false,bar, end]"#);
}

Expand All @@ -87,7 +88,7 @@ fn test_format_expand_pairs_from_stdin() {

let mut args = ["three", "3"].map(Cow::from).into_iter().enumerate();

let (res, _) = jf::format_partial(&mut chars, &mut args, &mut stdin).unwrap();
let (res, _, _) = jf::format_partial(&mut chars, &mut args, &mut stdin).unwrap();
assert_eq!(
res,
r#"{args: {"three":"3"}, stdin: {"one":"1","two":"2"}}"#
Expand Down Expand Up @@ -177,7 +178,7 @@ fn test_format_named_from_stdin() {
.enumerate();
let mut args = ["FOO@-", "BAR@-"].map(Cow::from).into_iter().enumerate();

let (res, _) = jf::format_partial(&mut chars, &mut args, &mut stdin).unwrap();
let (res, _, _) = jf::format_partial(&mut chars, &mut args, &mut stdin).unwrap();

assert_eq!(res, r#"{"foo": "bar"}"#);
}
Expand Down Expand Up @@ -217,7 +218,7 @@ fn test_format_named_with_default_from_stdin() {
.into_iter()
.enumerate();

let (res, _) = jf::format_partial(&mut chars, &mut args, &mut stdin).unwrap();
let (res, _, _) = jf::format_partial(&mut chars, &mut args, &mut stdin).unwrap();
assert_eq!(res, r#""foo""#);

let mut chars = "%(foo@-)q".chars().enumerate();
Expand All @@ -228,7 +229,7 @@ fn test_format_named_with_default_from_stdin() {
.map(io::Result::Ok)
.into_iter()
.enumerate();
let (res, _) = jf::format_partial(&mut chars, &mut args, &mut stdin).unwrap();
let (res, _, _) = jf::format_partial(&mut chars, &mut args, &mut stdin).unwrap();
assert_eq!(res, r#""bar""#);
}

Expand Down Expand Up @@ -332,6 +333,30 @@ fn test_format_named_pairs() {
);
}

#[test]
fn test_raw_mode() {
let args = ["%R%*s", "1", "2", "3"].map(Into::into);
assert_eq!(jf::format(args).unwrap(), "1,2,3");

let args = ["%R%s %q, (%s)", "1", "2", "3"].map(Into::into);
assert_eq!(jf::format(args).unwrap(), r#"1 "2", (3)"#);
}

#[test]
fn test_yaml_mode() {
let args = ["%Y{a: b, c: d, e: [f, g]}"].map(Into::into);
assert_eq!(jf::format(args).unwrap(), "a: b\nc: d\ne:\n- f\n- g\n");
}

#[test]
fn test_pretty_json_mode() {
let args = ["%J{a: b, c: d, e: [f, g]}"].map(Into::into);
assert_eq!(
jf::format(args).unwrap(),
"{\n \"a\": \"b\",\n \"c\": \"d\",\n \"e\": [\n \"f\",\n \"g\"\n ]\n}"
);
}

#[test]
fn test_optional_placeholder_with_default_value_error() {
let args = [r#"%(foo=bar)?q"#].map(Into::into);
Expand Down Expand Up @@ -676,14 +701,14 @@ fn test_usage_example() {
#[test]
fn test_print_version() {
let arg = ["jf v%v"].map(Into::into);
assert_eq!(jf::format(arg).unwrap(), r#""jf v0.4.1""#);
assert_eq!(jf::format(arg).unwrap(), r#""jf v0.4.2""#);

let args =
["{foo: %q, bar: %(bar)q, version: %v}", "foo", "bar=bar"].map(Into::into);

assert_eq!(
jf::format(args).unwrap(),
r#"{"foo":"foo","bar":"bar","version":"0.4.1"}"#
r#"{"foo":"foo","bar":"bar","version":"0.4.2"}"#
);
}

Expand Down
3 changes: 3 additions & 0 deletions src/usage.txt
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ USAGE
`%s` JSON values other than string
`%v` the `jf` version number
`%%` a literal `%` character
`%R` enable raw mode - render but do not format output
`%Y` enable pretty YAML mode - format output into pretty YAML
`%J` enable pretty JSON mode - format output into pretty JSON

And [VALUE]... [NAME=VALUE]... [NAME@FILE]... are the values for the placeholders.

Expand Down

0 comments on commit cbbd746

Please sign in to comment.