Skip to content

Commit cd3e9e2

Browse files
authored
Merge branch 'master' into feat/build-script-log
2 parents 257c22f + 269c357 commit cd3e9e2

File tree

12 files changed

+676
-188
lines changed

12 files changed

+676
-188
lines changed

Cargo.toml

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,21 +8,22 @@ documentation = "https://docs.rs/cmd_lib"
88
keywords = ["shell", "script", "cli", "process", "pipe"]
99
categories = ["command-line-interface", "command-line-utilities"]
1010
readme = "README.md"
11-
version = "1.9.5"
11+
version = "1.9.6"
1212
authors = ["rust-shell-script <[email protected]>"]
1313
edition = "2018"
1414

1515
[workspace]
1616
members = ["macros"]
1717

1818
[dependencies]
19-
cmd_lib_macros = { version = "1.9.5", path = "./macros" }
19+
cmd_lib_macros = { version = "1.9.6", path = "./macros" }
2020
lazy_static = "1.4.0"
2121
log = "0.4.20"
2222
faccess = "0.2.4"
2323
os_pipe = "1.1.4"
2424
env_logger = "0.10.0"
25-
build-print = { version = "0.1.1", optional = true }
25+
build-print = { version = "1.0", optional = true }
26+
tracing = { version = "0.1.41", optional = true }
2627

2728
[dev-dependencies]
2829
rayon = "1.8.0"
@@ -31,3 +32,9 @@ byte-unit = "4.0.19"
3132

3233
[features]
3334
build-print = ["dep:build-print"]
35+
tracing = "0.1.41"
36+
tracing-subscriber = { version = "0.3.19", features = ["env-filter"] }
37+
38+
[[example]]
39+
name = "tracing"
40+
required-features = ["tracing"]

README.md

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -304,11 +304,10 @@ You can use [std::env::var](https://doc.rust-lang.org/std/env/fn.var.html) to fe
304304
key from the current process. It will report error if the environment variable is not present, and it also
305305
includes other checks to avoid silent failures.
306306

307-
To set environment variables, you can use [std::env::set_var](https://doc.rust-lang.org/std/env/fn.set_var.html).
308-
There are also other related APIs in the [std::env](https://doc.rust-lang.org/std/env/index.html) module.
307+
To set environment variables in **single-threaded programs**, you can use [std::env::set_var] and
308+
[std::env::remove_var]. While those functions **[must not be called]** if any other threads might be running, you can
309+
always set environment variables for one command at a time, by putting the assignments before the command:
309310

310-
To set environment variables for the command only, you can put the assignments before the command.
311-
Like this:
312311
```rust
313312
run_cmd!(FOO=100 /tmp/test_run_cmd_lib.sh)?;
314313
```
@@ -330,9 +329,23 @@ You can use the [glob](https://github.com/rust-lang-nursery/glob) package instea
330329

331330
#### Thread Safety
332331

333-
This library tries very hard to not set global states, so parallel `cargo test` can be executed just fine.
334-
The only known APIs not supported in multi-thread environment are the
335-
[`tls_init!`](https://docs.rs/cmd_lib/latest/cmd_lib/macro.tls_init.html)/[`tls_get!`](https://docs.rs/cmd_lib/latest/cmd_lib/macro.tls_get.html)/[`tls_set!`](https://docs.rs/cmd_lib/latest/cmd_lib/macro.tls_set.html) macros, and you should only use them for *thread local* variables.
332+
This library tries very hard to not set global state, so parallel `cargo test` can be executed just fine.
333+
That said, there are some limitations to be aware of:
334+
335+
- [std::env::set_var] and [std::env::remove_var] **[must not be called]** in a multi-threaded program
336+
- [`tls_init!`](https://docs.rs/cmd_lib/latest/cmd_lib/macro.tls_init.html),
337+
[`tls_get!`](https://docs.rs/cmd_lib/latest/cmd_lib/macro.tls_get.html), and
338+
[`tls_set!`](https://docs.rs/cmd_lib/latest/cmd_lib/macro.tls_set.html) create *thread-local* variables, which means
339+
each thread will have its own independent version of the variable
340+
- [`set_debug`](https://docs.rs/cmd_lib/latest/cmd_lib/fn.set_debug.html) and
341+
[`set_pipefail`](https://docs.rs/cmd_lib/latest/cmd_lib/fn.set_pipefail.html) are *global* and affect all threads;
342+
to change those settings without affecting other threads, use
343+
[`ScopedDebug`](https://docs.rs/cmd_lib/latest/cmd_lib/struct.ScopedDebug.html) and
344+
[`ScopedPipefail`](https://docs.rs/cmd_lib/latest/cmd_lib/struct.ScopedPipefail.html)
345+
346+
[std::env::set_var]: https://doc.rust-lang.org/std/env/fn.set_var.html
347+
[std::env::remove_var]: https://doc.rust-lang.org/std/env/fn.remove_var.html
348+
[must not be called]: https://doc.rust-lang.org/nightly/edition-guide/rust-2024/newly-unsafe-functions.html#stdenvset_var-remove_var
336349

337350

338351
License: MIT OR Apache-2.0

examples/dd_test.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,10 @@
1515
//! [INFO ] Total bandwidth: 1.11 GiB/s
1616
//! ```
1717
use byte_unit::Byte;
18+
use clap::Parser;
1819
use cmd_lib::*;
1920
use rayon::prelude::*;
2021
use std::time::Instant;
21-
use clap::Parser;
2222

2323
const DATA_SIZE: u64 = 10 * 1024 * 1024 * 1024; // 10GB data
2424

examples/progress.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
use cmd_lib::{run_cmd, CmdResult};
2+
3+
#[cmd_lib::main]
4+
fn main() -> CmdResult {
5+
run_cmd!(dd if=/dev/urandom of=/dev/null bs=1M status=progress)?;
6+
7+
Ok(())
8+
}

examples/tracing.rs

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
use cmd_lib::{run_cmd, CmdResult};
2+
use tracing::level_filters::LevelFilter;
3+
use tracing_subscriber::{layer::SubscriberExt as _, util::SubscriberInitExt as _, EnvFilter};
4+
5+
#[cmd_lib::main]
6+
fn main() -> CmdResult {
7+
tracing_subscriber::registry()
8+
.with(tracing_subscriber::fmt::layer().with_writer(std::io::stderr))
9+
.with(
10+
EnvFilter::builder()
11+
.with_default_directive(LevelFilter::INFO.into())
12+
.from_env_lossy(),
13+
)
14+
.init();
15+
16+
copy_thing()?;
17+
18+
Ok(())
19+
}
20+
21+
#[tracing::instrument]
22+
fn copy_thing() -> CmdResult {
23+
// Log output from stderr inherits the `copy_thing` span from this function
24+
run_cmd!(dd if=/dev/urandom of=/dev/null bs=1M count=1000)?;
25+
26+
Ok(())
27+
}

macros/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ license = "MIT OR Apache-2.0"
55
homepage = "https://github.com/rust-shell-script/rust_cmd_lib"
66
repository = "https://github.com/rust-shell-script/rust_cmd_lib"
77
keywords = ["shell", "script", "cli", "process", "pipe"]
8-
version = "1.9.5"
8+
version = "1.9.6"
99
authors = ["Tao Guo <[email protected]>"]
1010
edition = "2018"
1111

macros/src/lexer.rs

Lines changed: 100 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -2,80 +2,136 @@ use crate::parser::{ParseArg, Parser};
22
use proc_macro2::{token_stream, Delimiter, Ident, Literal, Span, TokenStream, TokenTree};
33
use proc_macro_error2::abort;
44
use quote::quote;
5-
use std::ffi::OsString;
65
use std::iter::Peekable;
6+
use std::str::Chars;
77

8-
// Scan string literal to tokenstream, used by most of the macros
9-
//
10-
// - support ${var} or $var for interpolation
11-
// - to escape '$' itself, use "$$"
12-
// - support normal rust character escapes:
13-
// https://doc.rust-lang.org/reference/tokens.html#ascii-escapes
8+
/// Scan string literal to tokenstream, used by most of the macros
9+
///
10+
/// - support $var, ${var} or ${var:fmt} for interpolation, where `fmt` can be any
11+
/// of the standard Rust formatting specifiers (e.g., `?`, `x`, `X`, `o`, `b`, `p`, `e`, `E`).
12+
/// - to escape '$' itself, use "$$"
13+
/// - support normal rust character escapes:
14+
/// https://doc.rust-lang.org/reference/tokens.html#ascii-escapes
1415
pub fn scan_str_lit(lit: &Literal) -> TokenStream {
1516
let s = lit.to_string();
17+
18+
// If the literal is not a string (e.g., a number literal), treat it as a direct CmdString.
1619
if !s.starts_with('\"') {
1720
return quote!(::cmd_lib::CmdString::from(#lit));
1821
}
19-
let mut iter = s[1..s.len() - 1] // To trim outside ""
20-
.chars()
21-
.peekable();
22+
23+
// Extract the inner string by trimming the surrounding quotes.
24+
let inner_str = &s[1..s.len() - 1];
25+
let mut chars = inner_str.chars().peekable();
2226
let mut output = quote!(::cmd_lib::CmdString::default());
23-
let mut last_part = OsString::new();
24-
fn seal_last_part(last_part: &mut OsString, output: &mut TokenStream) {
27+
let mut current_literal_part = String::new();
28+
29+
// Helper function to append the accumulated literal part to the output TokenStream
30+
// and clear the current_literal_part.
31+
let seal_current_literal_part = |output: &mut TokenStream, last_part: &mut String| {
2532
if !last_part.is_empty() {
26-
let lit_str = format!("\"{}\"", last_part.to_str().unwrap());
27-
let l = syn::parse_str::<Literal>(&lit_str).unwrap();
28-
output.extend(quote!(.append(#l)));
33+
let lit_str = format!("\"{}\"", last_part);
34+
// It's safe to unwrap parse_str because we are constructing a valid string literal.
35+
let literal_token = syn::parse_str::<Literal>(&lit_str).unwrap();
36+
output.extend(quote!(.append(#literal_token)));
2937
last_part.clear();
3038
}
31-
}
39+
};
3240

33-
while let Some(ch) = iter.next() {
41+
while let Some(ch) = chars.next() {
3442
if ch == '$' {
35-
if iter.peek() == Some(&'$') {
36-
iter.next();
37-
last_part.push("$");
43+
// Handle "$$" for escaping '$'
44+
if chars.peek() == Some(&'$') {
45+
chars.next(); // Consume the second '$'
46+
current_literal_part.push('$');
3847
continue;
3948
}
4049

41-
seal_last_part(&mut last_part, &mut output);
42-
let mut with_brace = false;
43-
if iter.peek() == Some(&'{') {
44-
with_brace = true;
45-
iter.next();
50+
// Before handling a variable, append any accumulated literal part.
51+
seal_current_literal_part(&mut output, &mut current_literal_part);
52+
53+
let mut format_specifier = String::new(); // To store the fmt specifier (e.g., "?", "x", "#x")
54+
let mut is_braced_interpolation = false;
55+
56+
// Check for '{' to start a braced interpolation
57+
if chars.peek() == Some(&'{') {
58+
is_braced_interpolation = true;
59+
chars.next(); // Consume '{'
4660
}
47-
let mut var = String::new();
48-
while let Some(&c) = iter.peek() {
49-
if !c.is_ascii_alphanumeric() && c != '_' {
50-
break;
61+
62+
let var_name = parse_variable_name(&mut chars);
63+
64+
if is_braced_interpolation {
65+
// If it's braced, we might have a format specifier or it might just be empty braces.
66+
if chars.peek() == Some(&':') {
67+
chars.next(); // Consume ':'
68+
// Read the format specifier until '}'
69+
while let Some(&c) = chars.peek() {
70+
if c == '}' {
71+
break;
72+
}
73+
format_specifier.push(c);
74+
chars.next(); // Consume the character of the specifier
75+
}
5176
}
52-
if var.is_empty() && c.is_ascii_digit() {
53-
break;
77+
78+
// Expect '}' to close the braced interpolation
79+
if chars.next() != Some('}') {
80+
abort!(lit.span(), "bad substitution: expected '}'");
5481
}
55-
var.push(c);
56-
iter.next();
5782
}
58-
if with_brace {
59-
if iter.peek() != Some(&'}') {
60-
abort!(lit.span(), "bad substitution");
83+
84+
if !var_name.is_empty() {
85+
let var_ident = syn::parse_str::<Ident>(&var_name).unwrap();
86+
87+
// To correctly handle all format specifiers (like {:02X}), we need to insert the
88+
// entire format string *as a literal* into the format! macro.
89+
// The `format_specifier` string itself needs to be embedded.
90+
let format_macro_call = if format_specifier.is_empty() {
91+
quote! {
92+
.append(format!("{}", #var_ident))
93+
}
6194
} else {
62-
iter.next();
63-
}
64-
}
65-
if !var.is_empty() {
66-
let var = syn::parse_str::<Ident>(&var).unwrap();
67-
output.extend(quote!(.append(#var.as_os_str())));
95+
let format_literal_str = format!("{{:{}}}", format_specifier);
96+
let format_literal_token = Literal::string(&format_literal_str);
97+
quote! {
98+
.append(format!(#format_literal_token, #var_ident))
99+
}
100+
};
101+
output.extend(format_macro_call);
68102
} else {
103+
// This covers cases like "${}" or "${:?}" with empty variable name
69104
output.extend(quote!(.append("$")));
70105
}
71106
} else {
72-
last_part.push(ch.to_string());
107+
current_literal_part.push(ch);
73108
}
74109
}
75-
seal_last_part(&mut last_part, &mut output);
110+
111+
// Append any remaining literal part after the loop finishes.
112+
seal_current_literal_part(&mut output, &mut current_literal_part);
76113
output
77114
}
78115

116+
/// Parses a variable name from the character iterator.
117+
/// A variable name consists of alphanumeric characters and underscores,
118+
/// and cannot start with a digit.
119+
fn parse_variable_name(chars: &mut Peekable<Chars<'_>>) -> String {
120+
let mut var = String::new();
121+
while let Some(&c) = chars.peek() {
122+
if !(c.is_ascii_alphanumeric() || c == '_') {
123+
break;
124+
}
125+
if var.is_empty() && c.is_ascii_digit() {
126+
// Variable names cannot start with a digit
127+
break;
128+
}
129+
var.push(c);
130+
chars.next(); // Consume the character
131+
}
132+
var
133+
}
134+
79135
enum SepToken {
80136
Space,
81137
SemiColon,

rustfmt.toml

Whitespace-only changes.

0 commit comments

Comments
 (0)