Repository: uutils/procps
Pull request #308 (2025-01-17 17:11:33)

sysctl: Add tool
This supports basic get and set of sysctls on Linux. Main missing thing is reading /etc/sysctl.d files.


Patch:
diff --git a/Cargo.lock b/Cargo.lock
index e03ba19..70ed98d 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -621,6 +621,7 @@ dependencies = [
  "uu_pwdx",
  "uu_slabtop",
  "uu_snice",
+ "uu_sysctl",
  "uu_top",
  "uu_w",
  "uu_watch",
@@ -1121,6 +1122,16 @@ dependencies = [
  "uucore",
 ]
 
+[[package]]
+name = "uu_sysctl"
+version = "0.0.1"
+dependencies = [
+ "clap",
+ "sysinfo",
+ "uucore",
+ "walkdir",
+]
+
 [[package]]
 name = "uu_top"
 version = "0.0.1"
diff --git a/Cargo.toml b/Cargo.toml
index e4d9217..b370523 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -36,6 +36,7 @@ feat_common_core = [
     "slabtop",
     "snice",
     "pkill",
+    "sysctl",
     "top",
     "w",
     "watch",
@@ -84,6 +85,7 @@ pwdx = { optional = true, version = "0.0.1", package = "uu_pwdx", path = "src/uu
 slabtop = { optional = true, version = "0.0.1", package = "uu_slabtop", path = "src/uu/slabtop" }
 snice = { optional = true, version = "0.0.1", package = "uu_snice", path = "src/uu/snice" }
 pkill = { optional = true, version = "0.0.1", package = "uu_pkill", path = "src/uu/pkill" }
+sysctl = { optional = true, version = "0.0.1", package = "uu_sysctl", path = "src/uu/sysctl" }
 top = { optional = true, version = "0.0.1", package = "uu_top", path = "src/uu/top" }
 w = { optional = true, version = "0.0.1", package = "uu_w", path = "src/uu/w" }
 watch = { optional = true, version = "0.0.1", package = "uu_watch", path = "src/uu/watch" }
diff --git a/src/uu/sysctl/Cargo.toml b/src/uu/sysctl/Cargo.toml
new file mode 100644
index 0000000..f426085
--- /dev/null
+++ b/src/uu/sysctl/Cargo.toml
@@ -0,0 +1,25 @@
+[package]
+name = "uu_sysctl"
+version = "0.0.1"
+edition = "2021"
+authors = ["uutils developers"]
+license = "MIT"
+description = "sysctl ~ (uutils) Show or modify kernel parameters at runtime"
+
+homepage = "https://github.com/uutils/procps"
+repository = "https://github.com/uutils/procps/tree/main/src/uu/sysctl"
+keywords = ["acl", "uutils", "cross-platform", "cli", "utility"]
+categories = ["command-line-utilities"]
+
+[dependencies]
+uucore = { workspace = true }
+clap = { workspace = true }
+sysinfo = { workspace = true }
+walkdir = { workspace = true }
+
+[lib]
+path = "src/sysctl.rs"
+
+[[bin]]
+name = "sysctl"
+path = "src/main.rs"
diff --git a/src/uu/sysctl/src/main.rs b/src/uu/sysctl/src/main.rs
new file mode 100644
index 0000000..7b14f30
--- /dev/null
+++ b/src/uu/sysctl/src/main.rs
@@ -0,0 +1,1 @@
+uucore::bin!(uu_sysctl);
diff --git a/src/uu/sysctl/src/sysctl.rs b/src/uu/sysctl/src/sysctl.rs
new file mode 100644
index 0000000..78aceb6
--- /dev/null
+++ b/src/uu/sysctl/src/sysctl.rs
@@ -0,0 +1,199 @@
+// This file is part of the uutils procps package.
+//
+// For the full copyright and license information, please view the LICENSE
+// file that was distributed with this source code.
+
+use clap::{crate_version, Arg, ArgAction, Command};
+use std::env;
+use uucore::error::UResult;
+use uucore::{format_usage, help_about, help_usage};
+
+const ABOUT: &str = help_about!("sysctl.md");
+const USAGE: &str = help_usage!("sysctl.md");
+
+#[cfg(target_os = "linux")]
+mod linux {
+    use std::path::{Path, PathBuf};
+    use uucore::error::{FromIo, UIoError};
+    use walkdir::WalkDir;
+
+    const PROC_SYS_ROOT: &str = "/proc/sys";
+
+    pub fn get_all_sysctl_variables() -> Vec<String> {
+        let mut ret = vec![];
+        for entry in WalkDir::new(PROC_SYS_ROOT) {
+            match entry {
+                Ok(e) => {
+                    if e.file_type().is_file() {
+                        let var = e
+                            .path()
+                            .strip_prefix(PROC_SYS_ROOT)
+                            .expect("Always should be ancestor of of sysctl root");
+                        if let Some(s) = var.as_os_str().to_str() {
+                            ret.push(s.to_owned());
+                        }
+                    }
+                }
+                Err(e) => {
+                    uucore::show_error!("{}", e);
+                }
+            }
+        }
+        ret
+    }
+
+    pub fn normalize_var(var: &str) -> String {
+        var.replace('/', ".")
+    }
+
+    pub fn variable_path(var: &str) -> PathBuf {
+        Path::new(PROC_SYS_ROOT).join(var.replace('.', "/"))
+    }
+
+    pub fn get_sysctl(var: &str) -> std::io::Result<String> {
+        Ok(std::fs::read_to_string(variable_path(var))?
+            .trim_end()
+            .to_string())
+    }
+
+    pub fn set_sysctl(var: &str, value: &str) -> std::io::Result<()> {
+        std::fs::write(variable_path(var), value)
+    }
+
+    pub fn handle_one_arg(
+        var_or_assignment: &str,
+        quiet: bool,
+    ) -> Result<Option<(String, String)>, Box<UIoError>> {
+        let mut splitted = var_or_assignment.splitn(2, '=');
+        let var = normalize_var(
+            splitted
+                .next()
+                .expect("Split always returns at least 1 value"),
+        );
+
+        if let Some(value_to_set) = splitted.next() {
+            set_sysctl(&var, value_to_set)
+                .map_err(|e| e.map_err_context(|| format!("error writing key '{}'", var)))?;
+            if quiet {
+                Ok(None)
+            } else {
+                Ok(Some((var, value_to_set.to_string())))
+            }
+        } else {
+            let value = get_sysctl(&var)
+                .map_err(|e| e.map_err_context(|| format!("error reading key '{}'", var)))?;
+            Ok(Some((var, value)))
+        }
+    }
+}
+#[cfg(target_os = "linux")]
+use linux::*;
+
+#[cfg(target_os = "linux")]
+#[uucore::main]
+pub fn uumain(args: impl uucore::Args) -> UResult<()> {
+    let matches = uu_app().try_get_matches_from(args)?;
+
+    let vars = if matches.get_flag("all") {
+        get_all_sysctl_variables()
+    } else if let Some(vars) = matches.get_many::<String>("variables") {
+        vars.cloned().collect()
+    } else {
+        uu_app().print_help()?;
+        return Ok(());
+    };
+
+    for var_or_assignment in vars {
+        match handle_one_arg(&var_or_assignment, matches.get_flag("quiet")) {
+            Ok(None) => (),
+            Ok(Some((var, value_to_print))) => {
+                for line in value_to_print.split('\n') {
+                    if matches.get_flag("names") {
+                        println!("{}", var);
+                    } else if matches.get_flag("values") {
+                        println!("{}", line);
+                    } else {
+                        println!("{} = {}", var, line);
+                    }
+                }
+            }
+            Err(e) => {
+                if !matches.get_flag("ignore") {
+                    uucore::show!(e);
+                }
+            }
+        }
+    }
+
+    Ok(())
+}
+
+#[cfg(not(target_os = "linux"))]
+#[uucore::main]
+pub fn uumain(args: impl uucore::Args) -> UResult<()> {
+    let _matches: clap::ArgMatches = uu_app().try_get_matches_from(args)?;
+
+    Err(uucore::error::USimpleError::new(
+        1,
+        "`sysctl` currently only supports Linux.",
+    ))
+}
+
+pub fn uu_app() -> Command {
+    Command::new(uucore::util_name())
+        .version(crate_version!())
+        .about(ABOUT)
+        .override_usage(format_usage(USAGE))
+        .infer_long_args(true)
+        .arg(
+            Arg::new("variables")
+                .value_name("VARIABLE[=VALUE]")
+                .action(ArgAction::Append),
+        )
+        .arg(
+            Arg::new("all")
+                .short('a')
+                .visible_short_aliases(['A', 'X'])
+                .long("all")
+                .action(ArgAction::SetTrue)
+                .help("Display all variables"),
+        )
+        .arg(
+            Arg::new("names")
+                .short('N')
+                .long("names")
+                .action(ArgAction::SetTrue)
+                .help("Only print names"),
+        )
+        .arg(
+            Arg::new("values")
+                .short('n')
+                .long("values")
+                .action(ArgAction::SetTrue)
+                .help("Only print values"),
+        )
+        .arg(
+            Arg::new("ignore")
+                .short('e')
+                .long("ignore")
+                .action(ArgAction::SetTrue)
+                .help("Ignore errors"),
+        )
+        .arg(
+            Arg::new("quiet")
+                .short('q')
+                .long("quiet")
+                .action(ArgAction::SetTrue)
+                .help("Do not print when setting variables"),
+        )
+        .arg(
+            Arg::new("noop_o")
+                .short('o')
+                .help("Does nothing, for BSD compatibility"),
+        )
+        .arg(
+            Arg::new("noop_x")
+                .short('x')
+                .help("Does nothing, for BSD compatibility"),
+        )
+}
diff --git a/src/uu/sysctl/sysctl.md b/src/uu/sysctl/sysctl.md
new file mode 100644
index 0000000..94e4b12
--- /dev/null
+++ b/src/uu/sysctl/sysctl.md
@@ -0,0 +1,7 @@
+# sysctl
+
+```
+sysctl [options] [variable[=value]]...
+```
+
+Show or modify kernel parameters at runtime.


Test patch:
diff --git a/tests/by-util/test_sysctl.rs b/tests/by-util/test_sysctl.rs
new file mode 100644
index 0000000..c3931bb
--- /dev/null
+++ b/tests/by-util/test_sysctl.rs
@@ -0,0 +1,81 @@
+// This file is part of the uutils procps package.
+//
+// For the full copyright and license information, please view the LICENSE
+// file that was distributed with this source code.
+
+use crate::common::util::TestScenario;
+
+#[test]
+fn test_invalid_arg() {
+    new_ucmd!().arg("--definitely-invalid").fails().code_is(1);
+}
+
+#[cfg(target_os = "linux")]
+mod linux {
+    use crate::common::util::TestScenario;
+
+    #[test]
+    fn test_get_simple() {
+        new_ucmd!()
+            .arg("kernel.ostype")
+            .arg("fs.overflowuid")
+            .succeeds()
+            .stdout_is("kernel.ostype = Linux\nfs.overflowuid = 65534\n");
+    }
+
+    #[test]
+    fn test_get_value_only() {
+        new_ucmd!()
+            .arg("-n")
+            .arg("kernel.ostype")
+            .arg("fs.overflowuid")
+            .succeeds()
+            .stdout_is("Linux\n65534\n");
+    }
+
+    #[test]
+    fn test_get_key_only() {
+        new_ucmd!()
+            .arg("-N")
+            .arg("kernel.ostype")
+            .arg("fs.overflowuid")
+            .succeeds()
+            .stdout_is("kernel.ostype\nfs.overflowuid\n");
+    }
+
+    #[test]
+    fn test_continues_on_error() {
+        new_ucmd!()
+            .arg("nonexisting")
+            .arg("kernel.ostype")
+            .fails()
+            .stdout_is("kernel.ostype = Linux\n")
+            .stderr_is("sysctl: error reading key 'nonexisting': No such file or directory\n");
+    }
+
+    #[test]
+    fn test_ignoring_errors() {
+        new_ucmd!()
+            .arg("-e")
+            .arg("nonexisting")
+            .arg("nonexisting2=foo")
+            .arg("kernel.ostype")
+            .succeeds()
+            .stdout_is("kernel.ostype = Linux\n")
+            .stderr_is("");
+    }
+}
+
+#[cfg(not(target_os = "linux"))]
+mod non_linux {
+    use crate::common::util::TestScenario;
+
+    #[test]
+    fn test_fails_on_unsupported_platforms() {
+        new_ucmd!()
+            .arg("-a")
+            .fails()
+            .code_is(1)
+            .stderr_is("sysctl: `sysctl` currently only supports Linux.\n");
+    }
+}
diff --git a/tests/tests.rs b/tests/tests.rs
index 77ce68f..2749cba 100644
--- a/tests/tests.rs
+++ b/tests/tests.rs
@@ -56,3 +56,7 @@ mod test_snice;
 #[cfg(feature = "pkill")]
 #[path = "by-util/test_pkill.rs"]
 mod test_pkill;
+
+#[cfg(feature = "sysctl")]
+#[path = "by-util/test_sysctl.rs"]
+mod test_sysctl;