1use super::{CommandExecutor, DockerCommand};
7use crate::error::Result;
8use async_trait::async_trait;
9use serde::{Deserialize, Serialize};
10
11#[derive(Debug, Clone)]
13#[allow(clippy::struct_excessive_bools)]
14pub struct PsCommand {
15 pub executor: CommandExecutor,
17 all: bool,
19 filters: Vec<String>,
21 format: Option<String>,
23 last: Option<i32>,
25 latest: bool,
27 no_trunc: bool,
29 quiet: bool,
31 size: bool,
33}
34
35#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
37pub struct ContainerInfo {
38 pub id: String,
40 pub image: String,
42 pub command: String,
44 pub created: String,
46 pub status: String,
48 pub ports: String,
50 pub names: String,
52}
53
54#[derive(Debug, Clone)]
56pub enum PsFormat {
57 Table,
59 Json,
61 Template(String),
63 Raw,
65}
66
67#[derive(Debug, Clone)]
69pub struct PsOutput {
70 pub stdout: String,
72 pub stderr: String,
74 pub exit_code: i32,
76 pub containers: Vec<ContainerInfo>,
78}
79
80impl PsOutput {
81 #[must_use]
83 pub fn success(&self) -> bool {
84 self.exit_code == 0
85 }
86
87 #[must_use]
89 pub fn combined_output(&self) -> String {
90 if self.stderr.is_empty() {
91 self.stdout.clone()
92 } else if self.stdout.is_empty() {
93 self.stderr.clone()
94 } else {
95 format!("{}\n{}", self.stdout, self.stderr)
96 }
97 }
98
99 #[must_use]
101 pub fn stdout_is_empty(&self) -> bool {
102 self.stdout.trim().is_empty()
103 }
104
105 #[must_use]
107 pub fn stderr_is_empty(&self) -> bool {
108 self.stderr.trim().is_empty()
109 }
110
111 #[must_use]
113 pub fn container_ids(&self) -> Vec<String> {
114 self.stdout
115 .lines()
116 .map(|line| line.trim().to_string())
117 .filter(|line| !line.is_empty())
118 .collect()
119 }
120
121 #[must_use]
123 pub fn container_count(&self) -> usize {
124 self.containers.len()
125 }
126}
127
128impl PsCommand {
129 #[must_use]
139 pub fn new() -> Self {
140 Self {
141 executor: CommandExecutor::new(),
142 all: false,
143 filters: Vec::new(),
144 format: None,
145 last: None,
146 latest: false,
147 no_trunc: false,
148 quiet: false,
149 size: false,
150 }
151 }
152
153 #[must_use]
163 pub fn all(mut self) -> Self {
164 self.all = true;
165 self
166 }
167
168 #[must_use]
180 pub fn filter(mut self, filter: impl Into<String>) -> Self {
181 self.filters.push(filter.into());
182 self
183 }
184
185 #[must_use]
196 pub fn filters(mut self, filters: Vec<String>) -> Self {
197 self.filters.extend(filters);
198 self
199 }
200
201 #[must_use]
211 pub fn format_table(mut self) -> Self {
212 self.format = Some("table".to_string());
213 self
214 }
215
216 #[must_use]
226 pub fn format_json(mut self) -> Self {
227 self.format = Some("json".to_string());
228 self
229 }
230
231 #[must_use]
242 pub fn format_template(mut self, template: impl Into<String>) -> Self {
243 self.format = Some(template.into());
244 self
245 }
246
247 #[must_use]
257 pub fn last(mut self, n: i32) -> Self {
258 self.last = Some(n);
259 self
260 }
261
262 #[must_use]
272 pub fn latest(mut self) -> Self {
273 self.latest = true;
274 self
275 }
276
277 #[must_use]
287 pub fn no_trunc(mut self) -> Self {
288 self.no_trunc = true;
289 self
290 }
291
292 #[must_use]
302 pub fn quiet(mut self) -> Self {
303 self.quiet = true;
304 self
305 }
306
307 #[must_use]
317 pub fn size(mut self) -> Self {
318 self.size = true;
319 self
320 }
321
322 fn parse_table_output(output: &str) -> Vec<ContainerInfo> {
324 let lines: Vec<&str> = output.lines().collect();
325 if lines.len() < 2 {
326 return Vec::new(); }
328
329 let mut containers = Vec::new();
330
331 for line in lines.iter().skip(1) {
333 if line.trim().is_empty() {
334 continue;
335 }
336
337 let parts: Vec<&str> = line.split_whitespace().collect();
339 if parts.len() >= 6 {
340 containers.push(ContainerInfo {
341 id: parts[0].to_string(),
342 image: parts[1].to_string(),
343 command: (*parts.get(2).unwrap_or(&"")).to_string(),
344 created: (*parts.get(3).unwrap_or(&"")).to_string(),
345 status: (*parts.get(4).unwrap_or(&"")).to_string(),
346 ports: (*parts.get(5).unwrap_or(&"")).to_string(),
347 names: (*parts.get(6).unwrap_or(&"")).to_string(),
348 });
349 }
350 }
351
352 containers
353 }
354
355 fn parse_json_output(output: &str) -> Vec<ContainerInfo> {
357 if let Ok(containers) = serde_json::from_str::<Vec<serde_json::Value>>(output) {
359 return containers
360 .into_iter()
361 .filter_map(|container| {
362 Some(ContainerInfo {
363 id: container.get("ID")?.as_str()?.to_string(),
364 image: container.get("Image")?.as_str()?.to_string(),
365 command: container.get("Command")?.as_str()?.to_string(),
366 created: container.get("CreatedAt")?.as_str()?.to_string(),
367 status: container.get("Status")?.as_str()?.to_string(),
368 ports: container.get("Ports")?.as_str().unwrap_or("").to_string(),
369 names: container.get("Names")?.as_str()?.to_string(),
370 })
371 })
372 .collect();
373 }
374
375 Vec::new()
376 }
377
378 #[must_use]
380 pub fn get_executor(&self) -> &CommandExecutor {
381 &self.executor
382 }
383
384 pub fn get_executor_mut(&mut self) -> &mut CommandExecutor {
386 &mut self.executor
387 }
388
389 #[must_use]
391 pub fn build_command_args(&self) -> Vec<String> {
392 let mut args = vec!["ps".to_string()];
393
394 if self.all {
395 args.push("--all".to_string());
396 }
397
398 for filter in &self.filters {
399 args.push("--filter".to_string());
400 args.push(filter.clone());
401 }
402
403 if let Some(ref format) = self.format {
404 args.push("--format".to_string());
405 args.push(format.clone());
406 }
407
408 if let Some(last) = self.last {
409 args.push("--last".to_string());
410 args.push(last.to_string());
411 }
412
413 if self.latest {
414 args.push("--latest".to_string());
415 }
416
417 if self.no_trunc {
418 args.push("--no-trunc".to_string());
419 }
420
421 if self.quiet {
422 args.push("--quiet".to_string());
423 }
424
425 if self.size {
426 args.push("--size".to_string());
427 }
428
429 args.extend(self.executor.raw_args.clone());
431
432 args
433 }
434}
435
436impl Default for PsCommand {
437 fn default() -> Self {
438 Self::new()
439 }
440}
441
442#[async_trait]
443impl DockerCommand for PsCommand {
444 type Output = PsOutput;
445
446 fn get_executor(&self) -> &CommandExecutor {
447 &self.executor
448 }
449
450 fn get_executor_mut(&mut self) -> &mut CommandExecutor {
451 &mut self.executor
452 }
453
454 fn build_command_args(&self) -> Vec<String> {
455 self.build_command_args()
456 }
457
458 async fn execute(&self) -> Result<Self::Output> {
459 let args = self.build_command_args();
460 let output = self.execute_command(args).await?;
461
462 let containers = if self.quiet {
464 Vec::new()
466 } else if let Some(ref format) = self.format {
467 if format == "json" {
468 Self::parse_json_output(&output.stdout)
469 } else {
470 Self::parse_table_output(&output.stdout)
471 }
472 } else {
473 Self::parse_table_output(&output.stdout)
475 };
476
477 Ok(PsOutput {
478 stdout: output.stdout,
479 stderr: output.stderr,
480 exit_code: output.exit_code,
481 containers,
482 })
483 }
484}
485
486#[cfg(test)]
487mod tests {
488 use super::*;
489
490 #[test]
491 fn test_ps_command_builder() {
492 let cmd = PsCommand::new()
493 .all()
494 .filter("status=running")
495 .filter("name=web")
496 .format_json()
497 .no_trunc()
498 .size();
499
500 let args = cmd.build_command_args();
501
502 assert!(args.contains(&"--all".to_string()));
503 assert!(args.contains(&"--filter".to_string()));
504 assert!(args.contains(&"status=running".to_string()));
505 assert!(args.contains(&"name=web".to_string()));
506 assert!(args.contains(&"--format".to_string()));
507 assert!(args.contains(&"json".to_string()));
508 assert!(args.contains(&"--no-trunc".to_string()));
509 assert!(args.contains(&"--size".to_string()));
510 }
511
512 #[test]
513 fn test_ps_command_quiet() {
514 let cmd = PsCommand::new().quiet().all();
515
516 let args = cmd.build_command_args();
517
518 assert!(args.contains(&"--quiet".to_string()));
519 assert!(args.contains(&"--all".to_string()));
520 }
521
522 #[test]
523 fn test_ps_command_latest() {
524 let cmd = PsCommand::new().latest();
525
526 let args = cmd.build_command_args();
527
528 assert!(args.contains(&"--latest".to_string()));
529 }
530
531 #[test]
532 fn test_ps_command_last() {
533 let cmd = PsCommand::new().last(5);
534
535 let args = cmd.build_command_args();
536
537 assert!(args.contains(&"--last".to_string()));
538 assert!(args.contains(&"5".to_string()));
539 }
540
541 #[test]
542 fn test_ps_command_multiple_filters() {
543 let filters = vec!["status=running".to_string(), "name=web".to_string()];
544 let cmd = PsCommand::new().filters(filters);
545
546 let args = cmd.build_command_args();
547
548 let filter_count = args.iter().filter(|&arg| arg == "--filter").count();
550 assert_eq!(filter_count, 2);
551 assert!(args.contains(&"status=running".to_string()));
552 assert!(args.contains(&"name=web".to_string()));
553 }
554
555 #[test]
556 fn test_ps_command_format_variants() {
557 let cmd1 = PsCommand::new().format_table();
558 assert!(cmd1.build_command_args().contains(&"table".to_string()));
559
560 let cmd2 = PsCommand::new().format_json();
561 assert!(cmd2.build_command_args().contains(&"json".to_string()));
562
563 let cmd3 = PsCommand::new().format_template("{{.ID}}");
564 assert!(cmd3.build_command_args().contains(&"{{.ID}}".to_string()));
565 }
566
567 #[test]
568 fn test_ps_output_helpers() {
569 let output = PsOutput {
570 stdout: "container1\ncontainer2\n".to_string(),
571 stderr: String::new(),
572 exit_code: 0,
573 containers: Vec::new(),
574 };
575
576 assert!(output.success());
577 assert!(!output.stdout_is_empty());
578 assert!(output.stderr_is_empty());
579
580 let ids = output.container_ids();
581 assert_eq!(ids.len(), 2);
582 assert_eq!(ids[0], "container1");
583 assert_eq!(ids[1], "container2");
584 }
585
586 #[test]
587 fn test_ps_command_extensibility() {
588 let mut cmd = PsCommand::new();
589
590 cmd.flag("--some-flag");
592 cmd.option("--some-option", "value");
593 cmd.arg("extra-arg");
594
595 let args = cmd.build_command_args();
596
597 assert!(args.contains(&"--some-flag".to_string()));
598 assert!(args.contains(&"--some-option".to_string()));
599 assert!(args.contains(&"value".to_string()));
600 assert!(args.contains(&"extra-arg".to_string()));
601 }
602
603 #[test]
604 fn test_container_info_creation() {
605 let info = ContainerInfo {
606 id: "abc123".to_string(),
607 image: "nginx:latest".to_string(),
608 command: "\"/docker-entrypoint.sh nginx -g 'daemon off;'\"".to_string(),
609 created: "2 minutes ago".to_string(),
610 status: "Up 2 minutes".to_string(),
611 ports: "0.0.0.0:8080->80/tcp".to_string(),
612 names: "web-server".to_string(),
613 };
614
615 assert_eq!(info.id, "abc123");
616 assert_eq!(info.image, "nginx:latest");
617 assert_eq!(info.names, "web-server");
618 }
619}