Skip to main content

linuxutils_misc/
logger.rs

1use linuxutils_common::man::ManContent;
2
3pub const MAN: ManContent = ManContent::empty();
4
5use clap::Parser;
6use std::{
7    io::{self, BufRead},
8    os::unix::net::UnixDatagram,
9    process::ExitCode,
10};
11
12const LOG_SOCKET: &str = "/dev/log";
13
14const FACILITIES: &[(&str, u8)] = &[
15    ("kern", 0),
16    ("user", 1),
17    ("mail", 2),
18    ("daemon", 3),
19    ("auth", 4),
20    ("syslog", 5),
21    ("lpr", 6),
22    ("news", 7),
23    ("uucp", 8),
24    ("cron", 9),
25    ("authpriv", 10),
26    ("ftp", 11),
27    ("local0", 16),
28    ("local1", 17),
29    ("local2", 18),
30    ("local3", 19),
31    ("local4", 20),
32    ("local5", 21),
33    ("local6", 22),
34    ("local7", 23),
35    ("security", 4),
36];
37
38const LEVELS: &[(&str, u8)] = &[
39    ("emerg", 0),
40    ("alert", 1),
41    ("crit", 2),
42    ("err", 3),
43    ("warning", 4),
44    ("notice", 5),
45    ("info", 6),
46    ("debug", 7),
47    ("panic", 0),
48    ("error", 3),
49    ("warn", 4),
50];
51
52#[derive(Parser)]
53#[command(name = "logger", about = "Enter messages into the system log")]
54pub struct Args {
55    /// Log the PID of the logger process
56    #[arg(short = 'i')]
57    log_pid: bool,
58
59    /// Log the contents of the specified file
60    #[arg(short = 'f', long = "file")]
61    file: Option<String>,
62
63    /// Skip empty lines when processing files
64    #[arg(short = 'e', long = "skip-empty")]
65    skip_empty: bool,
66
67    /// Set priority as facility.level or numeric (default: user.notice)
68    #[arg(short = 'p', long = "priority", default_value = "user.notice")]
69    priority: String,
70
71    /// Also output message to stderr
72    #[arg(short = 's', long = "stderr")]
73    stderr: bool,
74
75    /// Tag every line with this tag (default: username)
76    #[arg(short = 't', long = "tag")]
77    tag: Option<String>,
78
79    /// Write to the specified Unix socket instead of /dev/log
80    #[arg(short = 'u', long = "socket")]
81    socket: Option<String>,
82
83    /// Message to log
84    message: Vec<String>,
85}
86
87fn parse_facility(name: &str) -> Result<u8, String> {
88    for &(n, code) in FACILITIES {
89        if n.eq_ignore_ascii_case(name) {
90            return Ok(code);
91        }
92    }
93    Err(format!("unknown facility: {name}"))
94}
95
96fn parse_level(name: &str) -> Result<u8, String> {
97    for &(n, code) in LEVELS {
98        if n.eq_ignore_ascii_case(name) {
99            return Ok(code);
100        }
101    }
102    Err(format!("unknown level: {name}"))
103}
104
105fn parse_priority(s: &str) -> Result<u32, String> {
106    if let Ok(n) = s.parse::<u32>() {
107        return Ok(n);
108    }
109    let (fac_name, level_name) = s
110        .split_once('.')
111        .ok_or_else(|| format!("invalid priority: {s}"))?;
112    let fac = parse_facility(fac_name)? as u32;
113    let level = parse_level(level_name)? as u32;
114    Ok(fac * 8 + level)
115}
116
117fn format_message(
118    priority: u32,
119    tag: &str,
120    pid: Option<u32>,
121    message: &str,
122) -> String {
123    if let Some(pid) = pid {
124        format!("<{priority}>{tag}[{pid}]: {message}")
125    } else {
126        format!("<{priority}>{tag}: {message}")
127    }
128}
129
130fn default_tag() -> String {
131    std::env::var("USER")
132        .unwrap_or_else(|_| rustix::process::getuid().as_raw().to_string())
133}
134
135pub fn run(args: Args) -> ExitCode {
136    let priority = match parse_priority(&args.priority) {
137        Ok(p) => p,
138        Err(e) => {
139            eprintln!("logger: {e}");
140            return ExitCode::FAILURE;
141        }
142    };
143
144    let tag = args.tag.unwrap_or_else(default_tag);
145    let pid = if args.log_pid {
146        Some(std::process::id())
147    } else {
148        None
149    };
150
151    let socket_path = args.socket.as_deref().unwrap_or(LOG_SOCKET);
152    let socket = match UnixDatagram::unbound() {
153        Ok(s) => s,
154        Err(e) => {
155            eprintln!("logger: failed to create socket: {e}");
156            return ExitCode::FAILURE;
157        }
158    };
159
160    if let Err(e) = socket.connect(socket_path) {
161        eprintln!("logger: failed to connect to {socket_path}: {e}");
162        return ExitCode::FAILURE;
163    }
164
165    let mut failed = false;
166    let mut send = |message: &str| {
167        let formatted = format_message(priority, &tag, pid, message);
168        if args.stderr {
169            eprintln!("{tag}: {message}");
170        }
171        if let Err(e) = socket.send(formatted.as_bytes()) {
172            eprintln!("logger: send failed: {e}");
173            failed = true;
174        }
175    };
176
177    if let Some(ref file) = args.file {
178        let reader: Box<dyn BufRead> = if file == "-" {
179            Box::new(io::stdin().lock())
180        } else {
181            match std::fs::File::open(file) {
182                Ok(f) => Box::new(io::BufReader::new(f)),
183                Err(e) => {
184                    eprintln!("logger: failed to open {file}: {e}");
185                    return ExitCode::FAILURE;
186                }
187            }
188        };
189        for line in reader.lines() {
190            match line {
191                Ok(line) => {
192                    if args.skip_empty && line.is_empty() {
193                        continue;
194                    }
195                    send(&line);
196                }
197                Err(e) => {
198                    eprintln!("logger: read error: {e}");
199                    return ExitCode::FAILURE;
200                }
201            }
202        }
203    } else if !args.message.is_empty() {
204        send(&args.message.join(" "));
205    } else {
206        let stdin = io::stdin();
207        for line in stdin.lock().lines() {
208            match line {
209                Ok(line) => {
210                    if args.skip_empty && line.is_empty() {
211                        continue;
212                    }
213                    send(&line);
214                }
215                Err(e) => {
216                    eprintln!("logger: read error: {e}");
217                    return ExitCode::FAILURE;
218                }
219            }
220        }
221    }
222
223    if failed {
224        ExitCode::FAILURE
225    } else {
226        ExitCode::SUCCESS
227    }
228}