A simple CLI with Rust
This post will serve my memory for two purposes:
- How to write simple CLI parsers (in Rust!!) to further execute some other task.
- And most importantly, to document some of the configurations done to Doom Emacs to get a working Rust programming environment (for free!).
Simple CLI parsing with Rust
Let’s say we have to deal with a TestNG Suite that can be executed with the following options:
- Which (remote) testing environment will be used to execute a group
of tests. Can be any string, and if no value is provided,
qa1
should be used. - (Optionally) Which is the
xml
file specifying the tests that will be executed by TestNG. No provided value means “execute all tests”. - Which browser will be used. Valid options are:
- Chrome (the default)
- Firefox
- Edge
- Safari
- Maximum timeouts for the Selenium WebDriver (in seconds). Default is 5 seconds.
- Assume that we can accept a pattern expression (note: this is not exactly a regex expression, see Running a Single Test - Maven Failsafe Plugin), and if none is provided, then all tests should be executed.
Installing a rust toolchain
Install rustup for your OS. Answer the questions made by the installer to select a toolchain (example, the nightly toolchain).
Create a skeleton for a “Hello-World” application in rust using cargo
In the command line, use cargo to create a simple project:
cargo new test_executor
This should create a structure of files/directories like the following:
.
├── Cargo.toml
└── src
└── main.rs
This (first) parser will use the clap crate
(clap
is a “Command Line Argument Parser for Rust”) for the
main parsing functionality. So, let’s add that dependency to the
project:
cargo add clap --features derive
Let’s add one more library that provides a convenient derive macro
for the standard library’s std::error::Error
trait:
cargo add thiserror
Now our little skeleton should have 3 files:
.
├── Cargo.lock
├── Cargo.toml
└── src
└── main.rs
And Cargo.toml
should declare our dependencies. It might
look similar to this:
[package]
name = "test_executor"
version = "0.1.0"
edition = "2021"
[dependencies]
clap = { version = "4.5.18", features = ["derive"] }
thiserror = "1.0.63"
Sample code (for quick reference and modification)
The following code does the job:
use clap::Parser;
use std::{
fmt::{self, Debug, Display},
str::FromStr,
};
use thiserror::Error;
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
enum Browser {
#[default]
Chrome,
Firefox,
Edge,
Safari,
}
#[derive(Debug, Error)]
#[error("Unknown browser")]
struct UnknownBrowser;
impl FromStr for Browser {
type Err = UnknownBrowser;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_lowercase().as_str() {
"chrome" => Ok(Browser::Chrome),
"firefox" => Ok(Browser::Firefox),
"edge" => Ok(Browser::Edge),
"safari" => Ok(Browser::Safari),
_ => Err(UnknownBrowser),
}
}
}
impl Display for Browser {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Chrome => write!(f, "Chrome"),
Self::Firefox => write!(f, "Firefox"),
Self::Edge => write!(f, "Edge"),
Self::Safari => write!(f, "Safari"),
}
}
}
/// Simple command-line argument parser
#[derive(Parser, Debug)]
#[command(version, about, long_about = None)]
struct Args {
/// Name of the environment (and associated properties)
#[arg(short, long, default_value = "qa1")]
env: String,
/// Test suite to execute in src/test/resources/testsuites
#[arg(short, long)]
test_suite: Option<String>,
/// Browser to use for testing
#[arg(short, long, default_value_t)]
browser: Browser,
/// Selenium WebDriverWait value in seconds
#[arg(short, long, default_value_t = 5)]
wait: u8,
/// Pattern for test cases to execute with maven failsafe plugin
#[arg(short, long, default_value = ".*")]
pattern: String,
}
// cargo run -- --browser=Firefox --env=qa2 --wait=30 --test_suite=gui-testsuite.xml
fn main() {
let args = Args::parse();
println!("Environment: {}", args.env);
println!("Test suite: {:?}", args.test_suite);
println!("Browser: {:?}", args.browser);
println!("Selenium WebDriverWait: {} seconds", args.wait);
println!("Pattern for test cases: {:?}", args.pattern);
}
It can be compiled with
cargo build
And executed with something like
cargo run -- --browser=Firefox --env=qa2 --wait=30 --test_suite=gui-testsuite.xml
Once we’re happy with the end result, build with
cargo build --release
And a binary can be copied from
src/target/release/test_executor
to wherever you need it
(in your $PATH
).
Rust development environment with Doom Emacs
Enable lsp, rust and tree-sitter as well as debugger in
init.el
. Some excerpts ofinit.el
::tools (debugger +lsp) (lsp +peek) (tree-sitter) :lang (rust +lsp +tree-sitter)
My
config.el
(load! "my-defaults-config") (load! "my-banner-config") (load! "my-emoji-config") (load! "my-mu4e-config") (load! "my-company-config") (load! "my-yasnippet-config") (load! "my-tabnine-config") (load! "my-dap-config") (load! "my-gptel-config") ;; TODO: clean up order and setup for different languages in these 2 files (load! "my-rust-config") (load! "my-lsp-config") (load! "my-org-config") (load! "my-gui-appearance-config") (load! "my-spell-config") (load! "my-tree-sitter-config") (load! "my-clojure-config") (load! "my-api-testing-config") (load! "my-db-config") (load! "my-vterm-config") (load! "my-consult-omni-config")
My debugger config (
my-dap-config.el
). We’re relying on lldb(use-package dap-mode :config (dap-ui-mode) (dap-ui-controls-mode 1) (require 'dap-lldb) (require 'dap-gdb-lldb) ;; installs .extension/vscode (dap-gdb-lldb-setup) ;;(setq dap-gdb-lldb-path "/Users/oscarvarto/doom-emacs/.local/etc/dap-extension/vscode/webfreak.debug") ;;https://users.rust-lang.org/t/debugging-in-emacs-doom/99540/2 (require 'dap-codelldb) (dap-codelldb-setup) ;; TODO: Find a way to change the :program argument without hardcoding it's value (dap-register-debug-template "Rust::LLDB Run Configuration" (list :type "lldb" :request "launch" :name "LLDB::Run" :gdbpath "rust-lldb" :target nil :program "/Users/oscarvarto/rustCode/test_executor/target/debug/test_executor" :cwd nil)))
Contents of
my-rust-config.el
, considering that I’m using nushell to get the toolchain and rustic for an improved development experience:(custom-set-faces '(rustic-compilation-column ((t (:inherit compilation-column-number)))) '(rustic-compilation-line ((t (:foreground "LimeGreen"))))) (defun rustic-mode-auto-save-hook () "Enable auto-saving in rustic-mode buffers." (when buffer-file-name (setq-local compilation-ask-about-save nil))) (add-hook 'rustic-mode-hook 'rustic-mode-auto-save-hook) (setq rustic-rustfmt-args "+nightly") (setq rustic-rustfmt-config-alist '((hard_tabs . t) (skip_children . nil))) (use-package rust-mode :init (setq rust-mode-treesitter-derive t)) ;; <== Tree-sitter related! (use-package rustic :custom (let ((toolchain (or (getenv "RUST_TOOLCHAIN") (string-trim (shell-command-to-string "nu -c \"rustup show | lines | get 14 | split row ' ' | get 0\"")) ;; e.g. "1.77.2-aarch64-apple-darwin" "nightly"))) (setq rustic-analyzer-command `("rustup" "run" ,toolchain "rust-analyzer")))) (with-eval-after-load 'rustic-mode (add-hook 'rustic-mode-hook 'lsp-ui-mode) (add-hook 'flycheck-mode-hook #'flycheck-rust-setup)) ;; TODO: https://robert.kra.hn/posts/rust-emacs-setup/ (use-package lsp-mode :commands lsp :custom ;; what to use when checking on-save. "check" is default, I prefer clippy (lsp-rust-analyzer-cargo-watch-command "clippy") (lsp-eldoc-render-all t) (lsp-idle-delay 0.6) ;; enable / disable the hints as you prefer: (lsp-inlay-hint-enable t) ;; These are optional configurations. See https://emacs-lsp.github.io/lsp-mode/page/lsp-rust-analyzer/#lsp-rust-analyzer-display-chaining-hints for a full list (lsp-rust-analyzer-display-lifetime-elision-hints-enable "skip_trivial") (lsp-rust-analyzer-display-chaining-hints t) (lsp-rust-analyzer-display-lifetime-elision-hints-use-parameter-names nil) (lsp-rust-analyzer-display-closure-return-type-hints t) (lsp-rust-analyzer-display-parameter-hints nil) (lsp-rust-analyzer-display-reborrow-hints nil) :config (add-hook 'lsp-mode-hook 'lsp-ui-mode)) (require 'lsp-ui) (setq! lsp-ui-peek-always-show t lsp-ui-sideline-show-hover t lsp-ui-doc-enable t)
Adding
rustic
topackage.el
:(package! rustic :recipe (:repo "emacs-rustic/rustic"))
my-lsp-config.el
(use-package lsp-mode :init (setq lsp-keymap-prefix "C-c l") :hook ((java-mode . lsp) ;; if you want which-key integration (lsp-mode . lsp-enable-which-key-integration)) :commands lsp) (use-package lsp-ui :init (setq lsp-ui-doc-popup-enabled t lsp-ui-doc-popup-max-width 0.8 lsp-ui-peek-always-show t) :commands lsp-ui-mode) (use-package lsp-treemacs :commands lsp-treemacs-errors-list) ;; optionally if you want to use debugger (use-package dap-mode) ;; (use-package dap-LANGUAGE) to load the dap adapter for your language (load! "my-lsp-booster-config") (load! "my-lsp-java-config") (load! "my-lsp-nu-config") (load! "my-lsp-vtsls-config")
The (expected) contents for lsp-booster functionality.
- In my
init.el
:
(setenv "LSP_USE_PLISTS" "true")
my-lsp-booster-config.el
(after! lsp-mode (setq! lsp-file-watch-threshold 20000 lsp-inlay-hint-enable t)) (defun lsp-booster--advice-json-parse (old-fn &rest args) "Try to parse bytecode instead of json." (or (when (equal (following-char) ?#) (let ((bytecode (read (current-buffer)))) (when (byte-code-function-p bytecode) (funcall bytecode)))) (apply old-fn args))) (advice-add (if (progn (require 'json) (fboundp 'json-parse-buffer)) 'json-parse-buffer 'json-read) :around #'lsp-booster--advice-json-parse) (defun lsp-booster--advice-final-command (old-fn cmd &optional test?) "Prepend emacs-lsp-booster command to lsp CMD." (let ((orig-result (funcall old-fn cmd test?))) (if (and (not test?) ;; for check lsp-server-present? (not (file-remote-p default-directory)) ;; see lsp-resolve-final-command, it would add extra shell wrapper lsp-use-plists (not (functionp 'json-rpc-connection)) ;; native json-rpc (executable-find "emacs-lsp-booster")) (progn (message "Using emacs-lsp-booster for %s!" orig-result) (cons "emacs-lsp-booster" orig-result)) orig-result))) (advice-add 'lsp-resolve-final-command :around #'lsp-booster--advice-final-command)
Tree-sitter useful stuff:
(use-package! tree-sitter :config ;;(setq +tree-sitter-hl-enabled-modes '(not web-mode typescript-tsx-mode)) (setq +tree-sitter-hl-enabled-modes t) (add-hook 'tree-sitter-after-on-hook #'tree-sitter-hl-mode)) ;; Configure ts-fold (use-package! ts-fold :after tree-sitter :config ;; See also: https://github.com/emacs-tree-sitter/elisp-tree-sitter/issues/286 ;; (global-ts-fold-indicators-mode 1) ;; <- Breaks Doom initialization ;; Set global keybindings for `ts-fold'. (map! :leader (:prefix-map ("z" . "fold") :desc "Fold all" "a" #'ts-fold-close-all :desc "Unfold all" "A" #'ts-fold-open-all :desc "Fold current" "f" #'ts-fold-close :desc "Unfold current" "F" #'ts-fold-open :desc "Toggle fold" "t" #'ts-fold-toggle)) (require 'ts-fold-indicators) :hook (prog-mode . ts-fold-indicators-mode)) (require 'line-reminder) (add-hook! 'prog-mode-hook #'line-reminder-mode) (setq line-reminder-show-option 'indicators)
Native Debug (GDB/LLDB)
Check dap-mode page for Rust.
Thanks to this section of Robert Krahn’s post, I learned that lldb-mi could be used to debug Rust code.
Clone the lldb-mi repo
Build with
cmake
, as instructed. For me, I had to do:LLVM_DIR=/opt/homebrew/Cellar/llvm/18.1.8/lib/cmake cmake . LLVM_DIR=/opt/homebrew/Cellar/llvm/18.1.8/lib/cmake cmake --build .
Copy the binary
lldb-mi
binary to/usr/local/bin (a directory in my ~$PATH
).Part of the setup should call
(dap-gdb-lldb-setup)
, that will do important setup for debugging.To debug your program, execute
M-x dap-hydra
, and select theRust::LLDB Run Configuration
.