use std::sync::atomic::{AtomicBool, AtomicU8, Ordering};
use std::sync::Mutex;
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub enum Verbosity {
Quiet = 0,
Normal = 1,
Verbose = 2,
}
static LEVEL: AtomicU8 = AtomicU8::new(Verbosity::Normal as u8);
pub fn level() -> Verbosity {
match LEVEL.load(Ordering::Relaxed) {
0 => Verbosity::Quiet,
2 => Verbosity::Verbose,
_ => Verbosity::Normal,
}
}
pub fn shows(line: Verbosity) -> bool {
level() >= line
}
#[must_use = "the verbosity is reset when the scope is dropped"]
pub struct Scope(());
impl Scope {
pub fn new(level: Verbosity) -> Scope {
LEVEL.store(level as u8, Ordering::Relaxed);
Scope(())
}
}
impl Drop for Scope {
fn drop(&mut self) {
drop_held();
LEVEL.store(Verbosity::Normal as u8, Ordering::Relaxed);
}
}
static HELD: Mutex<Vec<String>> = Mutex::new(Vec::new());
static HOLDING: AtomicBool = AtomicBool::new(false);
fn held() -> std::sync::MutexGuard<'static, Vec<String>> {
HELD.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
}
pub fn hold(level: Verbosity, line: String) {
if !shows(level) {
return;
}
held().push(line);
HOLDING.store(true, Ordering::Release);
}
pub fn release_held() {
if !HOLDING.load(Ordering::Acquire) {
return;
}
let mut slot = held();
for line in slot.drain(..) {
eprintln!("{line}");
}
HOLDING.store(false, Ordering::Release);
}
pub fn drop_held() {
if !HOLDING.load(Ordering::Acquire) {
return;
}
held().clear();
HOLDING.store(false, Ordering::Release);
}
pub fn github_action_error(message: &str) {
let truthy = |name: &str| std::env::var(name).is_ok_and(|v| !v.is_empty() && v != "0");
if !truthy("GITHUB_ACTIONS") || truthy("COMPOSER_TESTS_ARE_RUNNING") {
return;
}
crate::out!("::error ::{}", escape_data(message));
}
fn escape_data(data: &str) -> String {
data.replace('%', "%25")
.replace('\r', "%0D")
.replace('\n', "%0A")
}
pub fn requested_verbosity<S: AsRef<str>>(args: &[S], shell_verbosity: Option<&str>) -> u8 {
let has = |value: &str| {
let leading = if value.starts_with("--") {
format!("{value}=")
} else {
value.to_owned()
};
args.iter()
.map(AsRef::as_ref)
.take_while(|t| *t != "--")
.any(|t| t == value || t.starts_with(&leading))
};
let base = match shell_verbosity.map(php_int).unwrap_or(0) {
-1 => 0,
1 => 2,
2 => 3,
3 => 4,
_ => 1,
};
if has("--quiet") || has("-q") {
0
} else if has("-vvv") || has("--verbose=3") {
4
} else if has("-vv") || has("--verbose=2") {
3
} else if has("-v") || has("--verbose=1") || has("--verbose") {
2
} else {
base
}
}
fn php_int(s: &str) -> i64 {
let t = s.trim_start_matches([' ', '\t', '\n', '\r', '\x0b', '\x0c']);
let b = t.as_bytes();
let mut i = 0;
if matches!(b.first(), Some(b'+' | b'-')) {
i += 1;
}
let digits = |from: usize| b[from..].iter().take_while(|c| c.is_ascii_digit()).count();
let int_len = digits(i);
i += int_len;
let mut float = false;
if b.get(i) == Some(&b'.') {
let frac = digits(i + 1);
if int_len + frac > 0 {
float = true;
i += 1 + frac;
}
}
if int_len == 0 && !float {
return 0;
}
if matches!(b.get(i), Some(b'e' | b'E')) {
let mut j = i + 1;
if matches!(b.get(j), Some(b'+' | b'-')) {
j += 1;
}
let exp = digits(j);
if exp > 0 {
float = true;
i = j + exp;
}
}
let text = &t[..i];
if float {
text.parse::<f64>().map(|f| f.trunc() as i64).unwrap_or(0)
} else {
text.parse::<i64>().unwrap_or(0)
}
}
#[macro_export]
macro_rules! say {
($($arg:tt)*) => {
$crate::say_at!($crate::io::Verbosity::Normal, $($arg)*)
};
}
#[macro_export]
macro_rules! say_always {
($($arg:tt)*) => {
$crate::say_at!($crate::io::Verbosity::Quiet, $($arg)*)
};
}
#[macro_export]
macro_rules! say_verbose {
($($arg:tt)*) => {
$crate::say_at!($crate::io::Verbosity::Verbose, $($arg)*)
};
}
#[macro_export]
macro_rules! say_at {
($level:expr, $($arg:tt)*) => {
if $crate::io::shows($level) {
$crate::io::release_held();
eprintln!($($arg)*);
}
};
}
#[macro_export]
macro_rules! out {
($($arg:tt)*) => {
if $crate::io::shows($crate::io::Verbosity::Normal) {
$crate::io::release_held();
println!($($arg)*);
}
};
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn github_annotation_escapes_like_escape_data() {
assert_eq!(
escape_data("100% sure\r\n Problem 1\n"),
"100%25 sure%0D%0A Problem 1%0A"
);
assert_eq!(escape_data("%0A"), "%250A");
}
#[test]
fn levels_and_symfony_parsing() {
assert_eq!(level(), Verbosity::Normal);
{
let _s = Scope::new(Verbosity::Quiet);
assert!(!shows(Verbosity::Normal));
assert!(shows(Verbosity::Quiet));
}
assert_eq!(level(), Verbosity::Normal);
{
let _s = Scope::new(Verbosity::Verbose);
assert!(shows(Verbosity::Verbose));
}
assert_eq!(level(), Verbosity::Normal);
let r = std::panic::catch_unwind(|| {
let _s = Scope::new(Verbosity::Quiet);
panic!("boom");
});
assert!(r.is_err());
assert_eq!(level(), Verbosity::Normal);
let v = |a: &[&str], e: Option<&str>| requested_verbosity(a, e);
assert_eq!(v(&["install"], None), 1);
assert_eq!(v(&["install", "-q"], None), 0);
assert_eq!(v(&["install", "--quiet"], None), 0);
assert_eq!(v(&["install", "-v"], None), 2);
assert_eq!(v(&["install", "--verbose"], None), 2);
assert_eq!(v(&["install", "--verbose=0"], None), 2);
assert_eq!(v(&["install", "-v", "-v"], None), 2);
assert_eq!(v(&["install", "-vv"], None), 3);
assert_eq!(v(&["install", "-vvv"], None), 4);
assert_eq!(v(&["install", "-vvvv"], None), 4);
assert_eq!(v(&["install", "--verbose=3"], None), 4);
assert_eq!(v(&["install", "-q", "-vvv"], None), 0);
assert_eq!(v(&["install", "--quietly"], None), 1);
assert_eq!(v(&["install"], Some("-1")), 0);
assert_eq!(v(&["install"], Some("1")), 2);
assert_eq!(v(&["install"], Some(" 1abc")), 2);
assert_eq!(php_int("1e1"), 10);
assert_eq!(php_int("1.9"), 1);
assert_eq!(php_int("-1.5"), -1);
assert_eq!(php_int(" 2e0x"), 2);
assert_eq!(php_int(".5"), 0);
assert_eq!(php_int("1e"), 1);
assert_eq!(php_int("x"), 0);
assert_eq!(v(&["install"], Some("1e1")), 1);
assert_eq!(v(&["install"], Some("-2")), 1);
assert_eq!(v(&["install"], Some("x")), 1);
assert_eq!(v(&["install", "-v"], Some("3")), 2);
assert_eq!(v(&["install"], Some("3")), 4);
assert_eq!(v(&["require", "--", "-q"], None), 1);
}
}