1use std::sync::atomic::{AtomicBool, AtomicU8, Ordering};
35use std::sync::Mutex;
36
37#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
39pub enum Verbosity {
40 Quiet = 0,
41 Normal = 1,
42 Verbose = 2,
43}
44
45static LEVEL: AtomicU8 = AtomicU8::new(Verbosity::Normal as u8);
46
47pub fn level() -> Verbosity {
49 match LEVEL.load(Ordering::Relaxed) {
50 0 => Verbosity::Quiet,
51 2 => Verbosity::Verbose,
52 _ => Verbosity::Normal,
53 }
54}
55
56pub fn shows(line: Verbosity) -> bool {
58 level() >= line
59}
60
61#[must_use = "the verbosity is reset when the scope is dropped"]
63pub struct Scope(());
64
65impl Scope {
66 pub fn new(level: Verbosity) -> Scope {
67 LEVEL.store(level as u8, Ordering::Relaxed);
68 Scope(())
69 }
70}
71
72impl Drop for Scope {
73 fn drop(&mut self) {
74 drop_held();
75 LEVEL.store(Verbosity::Normal as u8, Ordering::Relaxed);
76 }
77}
78
79static HELD: Mutex<Vec<String>> = Mutex::new(Vec::new());
82static HOLDING: AtomicBool = AtomicBool::new(false);
83
84fn held() -> std::sync::MutexGuard<'static, Vec<String>> {
86 HELD.lock()
87 .unwrap_or_else(std::sync::PoisonError::into_inner)
88}
89
90pub fn hold(level: Verbosity, line: String) {
94 if !shows(level) {
95 return;
96 }
97 held().push(line);
98 HOLDING.store(true, Ordering::Release);
99}
100
101pub fn release_held() {
106 if !HOLDING.load(Ordering::Acquire) {
107 return;
108 }
109 let mut slot = held();
110 for line in slot.drain(..) {
111 eprintln!("{line}");
112 }
113 HOLDING.store(false, Ordering::Release);
114}
115
116pub fn drop_held() {
120 if !HOLDING.load(Ordering::Acquire) {
121 return;
122 }
123 held().clear();
124 HOLDING.store(false, Ordering::Release);
125}
126
127pub fn github_action_error(message: &str) {
135 let truthy = |name: &str| std::env::var(name).is_ok_and(|v| !v.is_empty() && v != "0");
136 if !truthy("GITHUB_ACTIONS") || truthy("COMPOSER_TESTS_ARE_RUNNING") {
137 return;
138 }
139 crate::out!("::error ::{}", escape_data(message));
140}
141
142fn escape_data(data: &str) -> String {
145 data.replace('%', "%25")
146 .replace('\r', "%0D")
147 .replace('\n', "%0A")
148}
149
150pub fn requested_verbosity<S: AsRef<str>>(args: &[S], shell_verbosity: Option<&str>) -> u8 {
161 let has = |value: &str| {
162 let leading = if value.starts_with("--") {
163 format!("{value}=")
164 } else {
165 value.to_owned()
166 };
167 args.iter()
168 .map(AsRef::as_ref)
169 .take_while(|t| *t != "--")
170 .any(|t| t == value || t.starts_with(&leading))
171 };
172 let base = match shell_verbosity.map(php_int).unwrap_or(0) {
173 -1 => 0,
174 1 => 2,
175 2 => 3,
176 3 => 4,
177 _ => 1,
178 };
179 if has("--quiet") || has("-q") {
180 0
181 } else if has("-vvv") || has("--verbose=3") {
182 4
183 } else if has("-vv") || has("--verbose=2") {
184 3
185 } else if has("-v") || has("--verbose=1") || has("--verbose") {
186 2
187 } else {
188 base
189 }
190}
191
192fn php_int(s: &str) -> i64 {
197 let t = s.trim_start_matches([' ', '\t', '\n', '\r', '\x0b', '\x0c']);
198 let b = t.as_bytes();
199 let mut i = 0;
200 if matches!(b.first(), Some(b'+' | b'-')) {
201 i += 1;
202 }
203 let digits = |from: usize| b[from..].iter().take_while(|c| c.is_ascii_digit()).count();
204 let int_len = digits(i);
205 i += int_len;
206 let mut float = false;
207 if b.get(i) == Some(&b'.') {
208 let frac = digits(i + 1);
209 if int_len + frac > 0 {
210 float = true;
211 i += 1 + frac;
212 }
213 }
214 if int_len == 0 && !float {
215 return 0;
216 }
217 if matches!(b.get(i), Some(b'e' | b'E')) {
218 let mut j = i + 1;
219 if matches!(b.get(j), Some(b'+' | b'-')) {
220 j += 1;
221 }
222 let exp = digits(j);
223 if exp > 0 {
224 float = true;
225 i = j + exp;
226 }
227 }
228 let text = &t[..i];
229 if float {
230 text.parse::<f64>().map(|f| f.trunc() as i64).unwrap_or(0)
231 } else {
232 text.parse::<i64>().unwrap_or(0)
233 }
234}
235
236#[macro_export]
238macro_rules! say {
239 ($($arg:tt)*) => {
240 $crate::say_at!($crate::io::Verbosity::Normal, $($arg)*)
241 };
242}
243
244#[macro_export]
246macro_rules! say_always {
247 ($($arg:tt)*) => {
248 $crate::say_at!($crate::io::Verbosity::Quiet, $($arg)*)
249 };
250}
251
252#[macro_export]
254macro_rules! say_verbose {
255 ($($arg:tt)*) => {
256 $crate::say_at!($crate::io::Verbosity::Verbose, $($arg)*)
257 };
258}
259
260#[macro_export]
263macro_rules! say_at {
264 ($level:expr, $($arg:tt)*) => {
265 if $crate::io::shows($level) {
266 $crate::io::release_held();
267 eprintln!($($arg)*);
268 }
269 };
270}
271
272#[macro_export]
274macro_rules! out {
275 ($($arg:tt)*) => {
276 if $crate::io::shows($crate::io::Verbosity::Normal) {
277 $crate::io::release_held();
278 println!($($arg)*);
279 }
280 };
281}
282
283#[cfg(test)]
284mod tests {
285 use super::*;
286
287 #[test]
288 fn github_annotation_escapes_like_escape_data() {
289 assert_eq!(
290 escape_data("100% sure\r\n Problem 1\n"),
291 "100%25 sure%0D%0A Problem 1%0A"
292 );
293 assert_eq!(escape_data("%0A"), "%250A");
294 }
295
296 #[test]
298 fn levels_and_symfony_parsing() {
299 assert_eq!(level(), Verbosity::Normal);
300 {
301 let _s = Scope::new(Verbosity::Quiet);
302 assert!(!shows(Verbosity::Normal));
303 assert!(shows(Verbosity::Quiet));
304 }
305 assert_eq!(level(), Verbosity::Normal);
306 {
307 let _s = Scope::new(Verbosity::Verbose);
308 assert!(shows(Verbosity::Verbose));
309 }
310 assert_eq!(level(), Verbosity::Normal);
311 let r = std::panic::catch_unwind(|| {
312 let _s = Scope::new(Verbosity::Quiet);
313 panic!("boom");
314 });
315 assert!(r.is_err());
316 assert_eq!(level(), Verbosity::Normal);
317
318 let v = |a: &[&str], e: Option<&str>| requested_verbosity(a, e);
319 assert_eq!(v(&["install"], None), 1);
320 assert_eq!(v(&["install", "-q"], None), 0);
321 assert_eq!(v(&["install", "--quiet"], None), 0);
322 assert_eq!(v(&["install", "-v"], None), 2);
323 assert_eq!(v(&["install", "--verbose"], None), 2);
324 assert_eq!(v(&["install", "--verbose=0"], None), 2);
325 assert_eq!(v(&["install", "-v", "-v"], None), 2);
326 assert_eq!(v(&["install", "-vv"], None), 3);
327 assert_eq!(v(&["install", "-vvv"], None), 4);
328 assert_eq!(v(&["install", "-vvvv"], None), 4);
329 assert_eq!(v(&["install", "--verbose=3"], None), 4);
330 assert_eq!(v(&["install", "-q", "-vvv"], None), 0);
331 assert_eq!(v(&["install", "--quietly"], None), 1);
332 assert_eq!(v(&["install"], Some("-1")), 0);
334 assert_eq!(v(&["install"], Some("1")), 2);
335 assert_eq!(v(&["install"], Some(" 1abc")), 2);
336 assert_eq!(php_int("1e1"), 10);
338 assert_eq!(php_int("1.9"), 1);
339 assert_eq!(php_int("-1.5"), -1);
340 assert_eq!(php_int(" 2e0x"), 2);
341 assert_eq!(php_int(".5"), 0);
342 assert_eq!(php_int("1e"), 1);
343 assert_eq!(php_int("x"), 0);
344 assert_eq!(v(&["install"], Some("1e1")), 1);
345 assert_eq!(v(&["install"], Some("-2")), 1);
346 assert_eq!(v(&["install"], Some("x")), 1);
347 assert_eq!(v(&["install", "-v"], Some("3")), 2);
348 assert_eq!(v(&["install"], Some("3")), 4);
349 assert_eq!(v(&["require", "--", "-q"], None), 1);
350 }
351}