@@ -6,11 +6,13 @@ use crate::{CmdResult, FunResult};
66use faccess:: { AccessMode , PathExt } ;
77use lazy_static:: lazy_static;
88use os_pipe:: { self , PipeReader , PipeWriter } ;
9+ use std:: cell:: Cell ;
910use std:: collections:: HashMap ;
1011use std:: ffi:: { OsStr , OsString } ;
1112use std:: fmt;
1213use std:: fs:: { File , OpenOptions } ;
1314use std:: io:: { Error , ErrorKind , Result } ;
15+ use std:: marker:: PhantomData ;
1416use std:: mem:: take;
1517use std:: path:: { Path , PathBuf } ;
1618use 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`].
9498static 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`].
97103static 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
120126pub ( 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
124132pub ( 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) ]
0 commit comments