Skip to content

Commit 269c357

Browse files
authored
Merge pull request #85 from delan/non-global-debug-pipefail
Add non-global ways to set debug and pipefail modes
2 parents 3bbf340 + 9177f95 commit 269c357

File tree

4 files changed

+112
-12
lines changed

4 files changed

+112
-12
lines changed

README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -339,7 +339,9 @@ That said, there are some limitations to be aware of:
339339
each thread will have its own independent version of the variable
340340
- [`set_debug`](https://docs.rs/cmd_lib/latest/cmd_lib/fn.set_debug.html) and
341341
[`set_pipefail`](https://docs.rs/cmd_lib/latest/cmd_lib/fn.set_pipefail.html) are *global* and affect all threads;
342-
there is currently no way to change those settings without affecting other 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)
343345

344346
[std::env::set_var]: https://doc.rust-lang.org/std/env/fn.set_var.html
345347
[std::env::remove_var]: https://doc.rust-lang.org/std/env/fn.remove_var.html

src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -394,7 +394,7 @@ pub use log as inner_log;
394394
pub use logger::try_init_default_logger;
395395
#[doc(hidden)]
396396
pub use process::{register_cmd, AsOsStr, Cmd, CmdString, Cmds, GroupCmds, Redirect};
397-
pub use process::{set_debug, set_pipefail, CmdEnv};
397+
pub use process::{set_debug, set_pipefail, CmdEnv, ScopedDebug, ScopedPipefail};
398398

399399
mod builtins;
400400
mod child;

src/process.rs

Lines changed: 98 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,13 @@ use crate::{CmdResult, FunResult};
66
use faccess::{AccessMode, PathExt};
77
use lazy_static::lazy_static;
88
use os_pipe::{self, PipeReader, PipeWriter};
9+
use std::cell::Cell;
910
use std::collections::HashMap;
1011
use std::ffi::{OsStr, OsString};
1112
use std::fmt;
1213
use std::fs::{File, OpenOptions};
1314
use std::io::{Error, ErrorKind, Result};
15+
use std::marker::PhantomData;
1416
use std::mem::take;
1517
use std::path::{Path, PathBuf};
1618
use std::process::Command;
@@ -91,15 +93,19 @@ pub fn register_cmd(cmd: &'static str, func: FnFun) {
9193
CMD_MAP.lock().unwrap().insert(OsString::from(cmd), func);
9294
}
9395

96+
/// Whether debug mode is enabled globally.
97+
/// Can be overridden by the thread-local setting in [`DEBUG_OVERRIDE`].
9498
static DEBUG_ENABLED: LazyLock<AtomicBool> =
9599
LazyLock::new(|| AtomicBool::new(std::env::var("CMD_LIB_DEBUG") == Ok("1".into())));
96100

101+
/// Whether debug mode is enabled globally.
102+
/// Can be overridden by the thread-local setting in [`PIPEFAIL_OVERRIDE`].
97103
static PIPEFAIL_ENABLED: LazyLock<AtomicBool> =
98104
LazyLock::new(|| AtomicBool::new(std::env::var("CMD_LIB_PIPEFAIL") != Ok("0".into())));
99105

100106
/// Set debug mode or not, false by default.
101107
///
102-
/// This is **global**, and affects all threads.
108+
/// This is **global**, and affects all threads. To set it for the current thread only, use [`ScopedDebug`].
103109
///
104110
/// Setting environment variable CMD_LIB_DEBUG=0|1 has the same effect, but the environment variable is only
105111
/// checked once at an unspecified time, so the only reliable way to do that is when the program is first started.
@@ -109,7 +115,7 @@ pub fn set_debug(enable: bool) {
109115

110116
/// Set pipefail or not, true by default.
111117
///
112-
/// This is **global**, and affects all threads.
118+
/// This is **global**, and affects all threads. To set it for the current thread only, use [`ScopedPipefail`].
113119
///
114120
/// Setting environment variable CMD_LIB_DEBUG=0|1 has the same effect, but the environment variable is only
115121
/// checked once at an unspecified time, so the only reliable way to do that is when the program is first started.
@@ -118,11 +124,99 @@ pub fn set_pipefail(enable: bool) {
118124
}
119125

120126
pub(crate) fn debug_enabled() -> bool {
121-
DEBUG_ENABLED.load(SeqCst)
127+
DEBUG_OVERRIDE
128+
.get()
129+
.unwrap_or_else(|| DEBUG_ENABLED.load(SeqCst))
122130
}
123131

124132
pub(crate) fn pipefail_enabled() -> bool {
125-
PIPEFAIL_ENABLED.load(SeqCst)
133+
PIPEFAIL_OVERRIDE
134+
.get()
135+
.unwrap_or_else(|| PIPEFAIL_ENABLED.load(SeqCst))
136+
}
137+
138+
thread_local! {
139+
/// Whether debug mode is enabled in the current thread.
140+
/// None means to use the global setting in [`DEBUG_ENABLED`].
141+
static DEBUG_OVERRIDE: Cell<Option<bool>> = Cell::new(None);
142+
143+
/// Whether pipefail mode is enabled in the current thread.
144+
/// None means to use the global setting in [`PIPEFAIL_ENABLED`].
145+
static PIPEFAIL_OVERRIDE: Cell<Option<bool>> = Cell::new(None);
146+
}
147+
148+
/// Overrides the debug mode in the current thread, while the value is in scope.
149+
///
150+
/// Each override restores the previous value when dropped, so they can be nested.
151+
/// Since overrides are thread-local, these values can’t be sent across threads.
152+
///
153+
/// ```
154+
/// # use cmd_lib::{ScopedDebug, run_cmd};
155+
/// // Must give the variable a name, not just `_`
156+
/// let _debug = ScopedDebug::set(true);
157+
/// run_cmd!(echo hello world)?; // Will have debug on
158+
/// # Ok::<(), std::io::Error>(())
159+
/// ```
160+
// PhantomData field is equivalent to `impl !Send for Self {}`
161+
pub struct ScopedDebug(Option<bool>, PhantomData<*const ()>);
162+
163+
/// Overrides the pipefail mode in the current thread, while the value is in scope.
164+
///
165+
/// Each override restores the previous value when dropped, so they can be nested.
166+
/// Since overrides are thread-local, these values can’t be sent across threads.
167+
// PhantomData field is equivalent to `impl !Send for Self {}`
168+
///
169+
/// ```
170+
/// # use cmd_lib::{ScopedPipefail, run_cmd};
171+
/// // Must give the variable a name, not just `_`
172+
/// let _debug = ScopedPipefail::set(false);
173+
/// run_cmd!(false | true)?; // Will have pipefail off
174+
/// # Ok::<(), std::io::Error>(())
175+
/// ```
176+
pub struct ScopedPipefail(Option<bool>, PhantomData<*const ()>);
177+
178+
impl ScopedDebug {
179+
/// ```compile_fail
180+
/// let _: Box<dyn Send> = Box::new(cmd_lib::ScopedDebug::set(true));
181+
/// ```
182+
/// ```compile_fail
183+
/// let _: Box<dyn Sync> = Box::new(cmd_lib::ScopedDebug::set(true));
184+
/// ```
185+
#[doc(hidden)]
186+
pub fn test_not_send_not_sync() {}
187+
188+
pub fn set(enabled: bool) -> Self {
189+
let result = Self(DEBUG_OVERRIDE.get(), PhantomData);
190+
DEBUG_OVERRIDE.set(Some(enabled));
191+
result
192+
}
193+
}
194+
impl Drop for ScopedDebug {
195+
fn drop(&mut self) {
196+
DEBUG_OVERRIDE.set(self.0)
197+
}
198+
}
199+
200+
impl ScopedPipefail {
201+
/// ```compile_fail
202+
/// let _: Box<dyn Send> = Box::new(cmd_lib::ScopedPipefail::set(true));
203+
/// ```
204+
/// ```compile_fail
205+
/// let _: Box<dyn Sync> = Box::new(cmd_lib::ScopedPipefail::set(true));
206+
/// ```
207+
#[doc(hidden)]
208+
pub fn test_not_send_not_sync() {}
209+
210+
pub fn set(enabled: bool) -> Self {
211+
let result = Self(PIPEFAIL_OVERRIDE.get(), PhantomData);
212+
PIPEFAIL_OVERRIDE.set(Some(enabled));
213+
result
214+
}
215+
}
216+
impl Drop for ScopedPipefail {
217+
fn drop(&mut self) {
218+
PIPEFAIL_OVERRIDE.set(self.0)
219+
}
126220
}
127221

128222
#[doc(hidden)]

tests/test_macros.rs

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,7 @@ fn test_vars_in_str3() {
108108
}
109109

110110
#[test]
111+
// FIXME: doctests have no effect here, and we need to split these into one test per error
111112
/// ```compile_fail
112113
/// run_cmd!(echo "${msg0}").unwrap();
113114
/// assert_eq!(run_fun!(echo "${ msg }").unwrap(), "${ msg }");
@@ -144,15 +145,16 @@ fn test_pipe() {
144145
assert!(run_cmd!(false | wc).is_err());
145146
assert!(run_cmd!(echo xx | false | wc | wc | wc).is_err());
146147

147-
set_pipefail(false);
148+
let _pipefail = ScopedPipefail::set(false);
148149
assert!(run_cmd!(du -ah . | sort -hr | head -n 10).is_ok());
149-
set_pipefail(true);
150+
let _pipefail = ScopedPipefail::set(true);
150151

151152
let wc_cmd = "wc";
152153
assert!(run_cmd!(ls | $wc_cmd).is_ok());
154+
}
153155

154-
// test `ignore` command and pipefail mode
155-
// FIXME: make set_pipefail() thread safe, then move this to a separate test_ignore_and_pipefail()
156+
#[test]
157+
fn test_ignore_and_pipefail() {
156158
struct TestCase {
157159
/// Run the test case, returning whether the result `.is_ok()`.
158160
code: fn() -> bool,
@@ -270,20 +272,21 @@ fn test_pipe() {
270272
"{} when pipefail is on",
271273
case.code_str
272274
);
273-
set_pipefail(false);
275+
let _pipefail = ScopedPipefail::set(false);
274276
ok &= check_eq!(
275277
(case.code)(),
276278
case.expected_ok_pipefail_off,
277279
"{} when pipefail is off",
278280
case.code_str
279281
);
280-
set_pipefail(true);
282+
let _pipefail = ScopedPipefail::set(true);
281283
}
282284

283285
assert!(ok);
284286
}
285287

286288
#[test]
289+
// FIXME: doctests have no effect here, and we need to split these into one test per error
287290
/// ```compile_fail
288291
/// run_cmd!(ls > >&1).unwrap();
289292
/// run_cmd!(ls >>&1).unwrap();
@@ -355,6 +358,7 @@ fn test_current_dir() {
355358
}
356359

357360
#[test]
361+
// FIXME: doctests have no effect here, and we need to split these into one test per error
358362
/// ```compile_fail
359363
/// run_cmd!(ls / /x &>>> /tmp/f).unwrap();
360364
/// run_cmd!(ls / /x &> > /tmp/f).unwrap();

0 commit comments

Comments
 (0)