Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions src/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,16 @@
- [Dynamic Arrays (`ns_vec`)](data_structures/vectors.md)
- [Key-Value Maps (`ns_hashmap`)](data_structures/hashmap.md)

- [Command Execution (`ns_cmd`)](cmd/README.md)
- [Running Shell Commands](cmd/execution.md)
- [Capturing Stdout & Stderr](cmd/output-capturing.md)
- [Auto-Cleanup (RAII in C)](cmd/auto-cleanup.md)

- [Examples](examples/README.md)
- [Safe User Input](examples/safe-input.md)
- [Building a CLI Menu](examples/cli-menu.md)
- [String Manipulation](examples/string-manipulation.md)
- [Process Execution & Auto-Free](examples/process-exec.md)

- [Contributing](contributing/README.md)
- [Architecture Overview](contributing/architecture.md)
Expand Down
62 changes: 62 additions & 0 deletions src/cmd/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
# Command Execution (`ns_cmd`)

Executing shell commands in standard C usually involves reaching for the
`system()` function or wrestling with `popen()`.

Both approaches are fraught with legacy issues. `system()` offers absolutely no
way to capture the output of the command (it just prints directly to the
terminal), and `popen()` requires manually managing streams, allocating buffers,
and parsing raw bytes—often leading to memory leaks or buffer overflows.

The **`ns_cmd`** module completely replaces these legacy functions with a
modern, memory-safe, and highly ergonomic wrapper around Rust's
`std::process::Command`.

## The NextStd Solution

With `ns_cmd`, executing a command and capturing its exact output is safe and
synchronous.

* **Complete Stream Capture:** Both `stdout` and `stderr` are automatically
captured and placed into NextStd's blazingly fast
[Small String Optimized (`ns_string`)](../string/README.md) structs.
* **Exit Codes:** The exact exit code of the shell process is captured and
returned.
* **Smart Routing:** Pass either a standard C string literal (`"uname -a"`) or a
dynamically built `ns_string`. The macros figure it out automatically.
* **Zero Memory Leaks:** Thanks to the `ns_autocmd` macro, process output
structs automatically free their own internal string buffers the moment they
go out of scope.

## Quick Glance

Here is how simple it is to execute a command, read its output, and clean up the
memory—all in four lines of C:

```c
#include <nextstd/ns.h>
#include <nextstd/ns_cmd.h>

int main(void) {
// 1. Initialize the output struct with the auto-cleanup macro
ns_autocmd ns_cmd_output out = {0};

// 2. Run the command safely
ns_cmd_run("uname -a", &out);

// 3. Use the captured data!
ns_println("Status: {}", out.exit_code);
ns_println("Output: {}", out.stdout_data);

// 4. No free() required. Memory cleans itself up here!
return 0;
}
```

## Deep Dive

Explore the specific features of the `ns_cmd` module:

* [Running Shell Commands](execution.md)
* [Capturing Stdout & Stderr](output-capturing.md)
* [Auto-Cleanup (RAII in C)](auto-cleanup.md)
85 changes: 85 additions & 0 deletions src/cmd/auto-cleanup.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
# Auto-Cleanup (RAII in C)

One of the biggest pain points in standard C is manual memory management. If a
library dynamically allocates memory to capture a string or a file, the
developer is strictly responsible for calling `free()` when they are done.

If you forget, your program leaks memory. If you call it twice, your program
crashes.

Languages like C++ and Rust solved this decades ago using **RAII** (Resource
Acquisition Is Initialization) and destructors. When a variable goes out of
scope (reaches the closing brace `}` of its block), the compiler automatically
inserts the cleanup code.

With NextStd, you get that exact same magic in C.

## The `ns_autocmd` Macro

NextStd leverages a powerful, widely-supported GCC/Clang compiler extension
called `__attribute__((cleanup))`. We wrap this extension in a simple, ergonomic
macro called `ns_autocmd`.

When you prefix your `ns_cmd_output` struct with `ns_autocmd`, you are attaching
a hidden destructor to the variable. The moment that variable falls out of
scope, the compiler automatically intercepts it and safely frees any internal
`ns_string` heap allocations.

```c
#include <nextstd/ns.h>
#include <nextstd/ns_cmd.h>

void run_status_check() {
// 1. Declare the struct with the auto-cleanup macro
ns_autocmd ns_cmd_output out = {0};

// 2. Run the command
ns_cmd_run("uptime", &out);

// 3. Print the result
ns_println("System Uptime: {}", out.stdout_data);

// 4. End of scope. The compiler automatically calls
// ns_cmd_output_free(&out) right here! No memory leaks!
}
```

## ⚠️ The Golden Rule: Zero-Initialization

There is one critical rule when using `ns_autocmd`:
**You must always zero-initialize your struct (`= {0};`).**

```c
// ❌ DANGEROUS: Contains random stack garbage memory
ns_autocmd ns_cmd_output bad_out;

// ✅ SAFE: Memory is explicitly zeroed out
ns_autocmd ns_cmd_output good_out = {0};
```

**Why is this required?** In C, uninitialized stack variables contain random
"garbage" data left over in RAM.

If `ns_cmd_run` were to fail *before* it could successfully populate your struct
(for example, if the system couldn't spawn a shell), the destructor would
eventually run and try to `free()` that random garbage memory. This results in a
guaranteed segmentation fault.

By zero-initializing the struct (`= {0};`), you guarantee that if the command
fails early, the destructor simply sees `NULL` pointers and lengths of `0`, and
safely does nothing.

## Manual Cleanup

If you are compiling on a highly restrictive embedded compiler that does not
support GCC/Clang extensions, or if you simply prefer manual memory management,
you can still declare the struct normally and use the manual free function:

```c
ns_cmd_output out = {0};
ns_cmd_run("ls", &out);
ns_println("Output: {}", out.stdout_data);

// Manually free the internal strings
ns_cmd_output_free(&out);
```
77 changes: 77 additions & 0 deletions src/cmd/execution.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
# Running Shell Commands

The core of the `ns_cmd` module is the `ns_cmd_run` macro. Unlike standard C
functions that force you to strictly cast your types or use entirely different
function names for different inputs (like `print_int` vs `print_string`),
NextStd uses modern C11 features to do the heavy lifting for you.

## The `_Generic` Routing Magic

In standard C, if an execution function expected a dynamic string struct, you
would be forced to convert your simple string literals into structs every single
time you wanted to run a basic command.

NextStd solves this by turning `ns_cmd_run` into a `_Generic` macro wrapper.

When you call `ns_cmd_run`, the C compiler inspects the type of the argument you
passed and automatically routes it to the correct Rust FFI backend function:

* If you pass a `"string literal"`, it routes to `ns_cmd_run_cstr`.
* If you pass an `ns_string` struct, it routes to `ns_cmd_run_ns`.

### 1. Passing Standard C Strings

For 90% of your use cases, you will likely just pass a hardcoded string literal.
The macro accepts it natively:

```c
#include <nextstd/ns.h>
#include <nextstd/ns_cmd.h>

int main(void) {
ns_autocmd ns_cmd_output out = {0};

// The compiler sees a const char* and routes it automatically!
ns_cmd_run("ls -la /var/log", &out);

return 0;
}
```

### 2. Passing Dynamic `ns_string` Structs

If you are building a command dynamically based on user input or file parsing,
you can pass your NextStd `ns_string` directly into the exact same macro. No
unwrapping or `.ptr` access required:

```c
#include <nextstd/ns.h>
#include <nextstd/ns_cmd.h>
#include <nextstd/ns_string.h>

int main(void) {
ns_autocmd ns_cmd_output out = {0};
ns_string my_dynamic_cmd;

// Safely build the command
ns_string_new(&my_dynamic_cmd, "echo 'NextStd is awesome!'");

// Pass the struct directly!
ns_cmd_run(my_dynamic_cmd, &out);

// Clean up the input string
ns_string_free(&my_dynamic_cmd);

return 0;
}
```

## Under the Hood: `sh -c`

When the Rust backend receives your command (regardless of how it was routed),
it securely spawns a new process using `sh -c "<your_command>"`.

This mimics the exact behavior of C's `system()`, meaning you have full access
to standard shell features like piping (`|`), redirecting (`>`), and environment
variable expansion (`$USER`), while still running entirely within Rust's
memory-safe execution sandbox.
74 changes: 74 additions & 0 deletions src/cmd/output-capturing.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
# Capturing Stdout & Stderr

The fatal flaw of standard C's `system()` function is that it blindly dumps the
command's output directly to the user's terminal. If your C program actually
needs to *read* that output to parse a version number, check a status, or log an
error, `system()` is completely useless.

You are usually forced to use `popen()`, which requires manually allocating
buffers, reading chunks in a `while` loop, and praying you don't trigger a
buffer overflow.

NextStd completely eliminates this friction.

## The `ns_cmd_output` Struct

When you run a command using `ns_cmd_run`, the entire lifecycle of the process
is captured and packed into a single, clean C struct:

```c
typedef struct ns_cmd_output {
ns_string stdout_data;
ns_string stderr_data;
int exit_code;
} ns_cmd_output;
```

### 1. The Output Streams (`stdout_data` & `stderr_data`)

Instead of raw `char*` arrays that you have to manually resize, NextStd uses the
underlying Rust `std::process::Command` engine to capture the raw bytes of the
streams and convert them directly into memory-safe `ns_string` structs.

**The SSO Advantage:** Because these streams are backed by NextStd's Small
String Optimization (SSO), if your command outputs less than 24 bytes (like
simply printing `"OK"` or an exit status string),
**zero heap allocation occurs.** The output is stored directly inside the struct
on the stack, making it incredibly fast.

### 2. The Exit Code

NextStd captures the exact integer status code returned by the shell. You no
longer have to mess with C's cryptic `WEXITSTATUS` macros just to find out if
your command succeeded. If the process exits cleanly, `exit_code` will be `0`.

## Example: Parsing Success vs. Failure

Because NextStd strictly separates standard output from standard error, your C
program can easily build branching logic based on whether a command failed.

```c
#include <nextstd/ns.h>
#include <nextstd/ns_cmd.h>

int main(void) {
ns_autocmd ns_cmd_output out = {0};

// Attempting to read a file that doesn't exist
ns_cmd_run("cat /path/to/missing_file.txt", &out);

if (out.exit_code != 0) {
ns_println("Command failed with code: {}", out.exit_code);
// We can safely read the exact error message the shell generated
ns_println("Error Reason: {}", out.stderr_data);
} else {
ns_println("File Contents: {}", out.stdout_data);
}

return 0;
}
```

By default, these strings are dynamically allocated (if over 23 bytes) and
require cleanup. However, thanks to a powerful compiler extension, you rarely
ever need to free them manually.
1 change: 1 addition & 0 deletions src/examples/process-exec.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# Process Execution &amp; Auto-Free