├── .devcontainer └── devcontainer.json ├── .gitignore ├── Cargo.toml ├── LICENSE ├── README.md ├── examples ├── .keep ├── no-modules │ └── main.rs └── simple │ ├── Cargo.toml │ └── src │ └── main.rs ├── lab.md ├── resources ├── banner.svg └── releasing.md └── src ├── lib.rs └── main.rs /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | // For format details, see https://aka.ms/devcontainer.json. For config options, see the 2 | // README at: https://github.com/devcontainers/templates/tree/main/src/rust 3 | { 4 | "name": "Rust", 5 | // Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile 6 | "image": "mcr.microsoft.com/devcontainers/rust:0-1-bullseye", 7 | "customizations": { 8 | "vscode": { 9 | "extensions": [ 10 | "rust-lang.rust-analyzer", 11 | "vadimcn.vscode-lldb", 12 | "GitHub.copilot-nightly" 13 | ] 14 | } 15 | } 16 | 17 | // Use 'mounts' to make the cargo cache persistent in a Docker Volume. 18 | // "mounts": [ 19 | // { 20 | // "source": "devcontainer-cargo-cache-${devcontainerId}", 21 | // "target": "/usr/local/cargo", 22 | // "type": "volume" 23 | // } 24 | // ] 25 | 26 | // Features to add to the dev container. More info: https://containers.dev/features. 27 | // "features": {}, 28 | 29 | // Use 'forwardPorts' to make a list of ports inside the container available locally. 30 | // "forwardPorts": [], 31 | 32 | // Use 'postCreateCommand' to run commands after the container is created. 33 | // "postCreateCommand": "rustc --version", 34 | 35 | // Configure tool-specific properties. 36 | // "customizations": {}, 37 | 38 | // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root. 39 | // "remoteUser": "root" 40 | } 41 | 42 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | /target/ 4 | 5 | # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries 6 | # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html 7 | Cargo.lock 8 | 9 | # These are backup files generated by rustfmt 10 | **/*.rs.bk 11 | 12 | 13 | # Added by cargo 14 | 15 | /target 16 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "blkrs" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | clap = "4.2" 10 | serde_json = "1.0" 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Alfredo Deza 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # [![Coursera Course](./resources/banner.svg)](https://insight.paiml.com/nvd "Coursera Course") 2 | 3 | # Rust CLI Example 4 | 5 | A small Rust CLI example you can use to build on. With an emphasis on Linux and creating automation tools that solve a problem for you. This is the basis for DevOps principles that you can apply in day-to-day work. 6 | 7 | 💡 Are you just looking for a 👉 [Rust template](https://github.com/alfredodeza/rust-template) to get started easily with a project? The [template](https://github.com/alfredodeza/rust-template) has everything you need! 8 | 9 | This repository is part of the Python and Rust CLI tools course: 10 | 11 | - [1: Resources](https://github.com/alfredodeza/python-and-rust-tools) 12 | - [2: Python CLI](https://github.com/alfredodeza/python-cli-example) 13 | - [3: Rust CLI](https://github.com/alfredodeza/rust-cli-example) 👈 You are here! 14 | - [4: Python Advanced CLI](https://github.com/alfredodeza/advanced-python-cli) 15 | - [5: Rust Advanced CLI](https://github.com/alfredodeza/advanced-rust-cli) 16 | 17 | 18 | ## Practice Lab 19 | Use the [included practice lab](./lab.md) to apply the content you've learned in this week. Follow the steps to create your own repository and apply the requirements to complete the lab. 20 | 21 | ## Setting up your environment 22 | Rust development requires certain tools to be installed on your system. The easiest way to do this is to use the [rustup](https://rustup.rs/) tool. This will install the Rust compiler and Cargo, the Rust package manager. Although you can install it in Linux using the package manager, I recommend using `rustup`. Use the following command or go through the [rustup.rs](https://rustup.rs/) website to install it. 23 | 24 | ```bash 25 | curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh 26 | ``` 27 | 28 | This repository and video course focuses on the development side of command-line tools in Rust. It uses [Visual Studio Code](https://code.visualstudio.com/?WT.mc_id=academic-0000-alfredodeza) as the editor of choice. You can use any editor you like, but the instructions in this repository will be for VS Code. 29 | 30 | These are all the tools and editor extensions I recommend you install to get started: 31 | 32 | - [Visual Studio Code](https://code.visualstudio.com/?WT.mc_id=academic-0000-alfredodeza) 33 | - [Rust and Cargo tools](https://rustup.rs/) 34 | - [Rust Analyzer](https://marketplace.visualstudio.com/items?itemName=rust-lang.rust-analyzer&WT.mc_id=academic-0000-alfredodeza) 35 | - [GitHub Copilot](https://marketplace.visualstudio.com/items?itemName=GitHub.copilot&WT.mc_id=academic-0000-alfredodeza) 36 | 37 | As part of your development workflow, I highly suggest you use the following programs in the terminal regularly: 38 | 39 | - `cargo fmt` - Formats your code to the Rust standard 40 | - `cargo clippy` - Lints your code and helps you find errors and potential issues 41 | - `cargo check` - Checks your code for errors and allows you to fix them before compiling (which means its faster!) 42 | 43 | 44 | ## Resources 45 | Explore additional content that you can use to learn more about the topics covered in this course. 46 | 47 | - [Releasing](./resources/releasing.md) 48 | 49 | **Coursera Courses** 50 | 51 | - [Linux and Bash for Data Engineering](https://www.coursera.org/learn/linux-and-bash-for-data-engineering-duke) 52 | - [Open Source Platforms for MLOps](https://www.coursera.org/learn/open-source-platforms-duke) 53 | - [Python Essentials for MLOps](https://www.coursera.org/learn/python-essentials-mlops-duke) 54 | - [Web Applications and Command-Line tools for Data Engineering](https://www.coursera.org/learn/web-app-command-line-tools-for-data-engineering-duke) 55 | - [Python and Pandas for Data Engineering](https://www.coursera.org/learn/python-and-pandas-for-data-engineering-duke) 56 | - [Scripting with Python and SQL for Data Engineering](https://www.coursera.org/learn/scripting-with-python-sql-for-data-engineering-duke) 57 | 58 | **O'Reilly Courses and Books** 59 | 60 | - [Python for DevOps](https://www.oreilly.com/library/view/python-for-devops/9781492057680/) (Book) 61 | - [Practical MLOps](https://www.oreilly.com/library/view/practical-mlops/9781098103002/) (Book) 62 | - [Linux For Beginners](https://learning.oreilly.com/videos/-/27922450VIDEOPAIML/) (Video) 63 | - [GitHub Codespaces Course](https://learning.oreilly.com/videos/-/27724023VIDEOPAIML/) (Video) 64 | - [Python Command-line Tools course](https://learning.oreilly.com/videos/python-command-line/50131VIDEOPAIML/) (Video) 65 | -------------------------------------------------------------------------------- /examples/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alfredodeza/rust-cli-example/3b70f1ac2405df9b89b2a42c1ef67a6f5dc1e154/examples/.keep -------------------------------------------------------------------------------- /examples/no-modules/main.rs: -------------------------------------------------------------------------------- 1 | use std::process::Command; 2 | 3 | fn run_command(command: &str) -> String { 4 | let args: Vec<&str> = command.split(" ").collect(); 5 | let output = Command::new(args[0]) 6 | .args(&args[1..]) 7 | .output() 8 | .expect("Failed to execute command"); 9 | let stdout = String::from_utf8_lossy(&output.stdout); 10 | stdout.to_string() 11 | } 12 | 13 | fn run_lsblk(device: &str) -> serde_json::Value { 14 | let command = "lsblk -J -o NAME,SIZE,TYPE,MOUNTPOINT"; 15 | let output = run_command(command); 16 | let devices: serde_json::Value = serde_json::from_str(&output).unwrap(); 17 | let devices = devices["blockdevices"].as_array().unwrap(); 18 | for parent in devices { 19 | if parent["name"] == device { 20 | return parent.clone(); 21 | } 22 | if let Some(children) = parent["children"].as_array() { 23 | for child in children { 24 | if child["name"] == device { 25 | return child.clone(); 26 | } 27 | } 28 | } 29 | } 30 | panic!("Device not found"); 31 | } 32 | 33 | fn main() { 34 | let matches = clap::App::new("lsblk") 35 | .version("0.0.1") 36 | .author("Alfredo Deza") 37 | .about("lsblk in Rust") 38 | .arg( 39 | clap::Arg::with_name("device") 40 | .help("Device to query") 41 | .required(true) 42 | .index(1) 43 | ) 44 | .get_matches(); 45 | 46 | let device = matches.value_of("device").unwrap(); 47 | let output = serde_json::to_string(&run_lsblk(&device)).unwrap(); 48 | println!("{}", output); 49 | } 50 | -------------------------------------------------------------------------------- /examples/simple/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "blkrs" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | serde_json = "1.0" -------------------------------------------------------------------------------- /examples/simple/src/main.rs: -------------------------------------------------------------------------------- 1 | use std::process::Command; 2 | 3 | fn run_command(command: &str) -> String { 4 | let args: Vec<&str> = command.split(" ").collect(); 5 | let output = Command::new(args[0]) 6 | .args(&args[1..]) 7 | .output() 8 | .expect("Failed to execute command"); 9 | let stdout = String::from_utf8_lossy(&output.stdout); 10 | stdout.to_string() 11 | } 12 | 13 | fn run_lsblk(device: &str) -> serde_json::Value { 14 | let command = "lsblk -J -o NAME,SIZE,TYPE,MOUNTPOINT"; 15 | let output = run_command(command); 16 | let devices: serde_json::Value = serde_json::from_str(&output).unwrap(); 17 | let devices = devices["blockdevices"].as_array().unwrap(); 18 | for parent in devices { 19 | if parent["name"] == device { 20 | return parent.clone(); 21 | } 22 | if let Some(children) = parent["children"].as_array() { 23 | for child in children { 24 | if child["name"] == device { 25 | return child.clone(); 26 | } 27 | } 28 | } 29 | } 30 | panic!("Device not found"); 31 | } 32 | 33 | fn main() { 34 | let args: Vec = std::env::args().collect(); 35 | let device = args.last().unwrap(); 36 | let output = serde_json::to_string(&run_lsblk(&device)).unwrap(); 37 | println!("{}", output); 38 | } 39 | -------------------------------------------------------------------------------- /lab.md: -------------------------------------------------------------------------------- 1 | # Practice Lab: Build a Command Line tool to list files in a directory 2 | 3 | In this lab, you will build a command line tool that lists files in a directory. The tool will take a directory path as an argument and list all the files in the directory. By default, if no directory path is provided, the tool will list files in the current directory. 4 | 5 | **Learning Objectives** 6 | 7 | - Create a command line tool using the Rust programming language 8 | - Use the `Clap` framework to parse command line arguments 9 | - Use the `std::fs` module to read files and directories 10 | 11 | **Steps:** 12 | 13 | For an easy setup with all dependencies installed and a pre-configured Rust and Cargo, [open the repository with Codespaces](https://codespaces.new/alfredodeza/rust-cli-example?quickstart=1) 14 | 15 | Alternate setup: Create a new repository in your account for your Rust project. Use this link to [create it in one step](https://github.com/alfredodeza/rust-template/generate). 16 | 17 | 1. Use the Clap framework to parse command line arguments. Use the [simple example in this repository](./examples/simple/src/main.rs) as a reference 18 | 1. Use the `std::fs` module to read files. For example: 19 | 20 | ```rust 21 | use std::fs; 22 | 23 | fn main() { 24 | let path = "."; 25 | let paths = fs::read_dir(path).unwrap(); 26 | for path in paths { 27 | println!("Name: {}", path.unwrap().path().display()) 28 | } 29 | } 30 | ``` 31 | 32 | **Bonus challenge:** Add options to your tool to list files recursively and to list hidden files. 33 | 34 | **Concepts Covered:** 35 | 36 | - [x] Build a command line tool using the Rust programming language 37 | - [x] Use the `Clap` framework to parse command line arguments 38 | - [x] Using the Rust standard library to read files and directories 39 | 40 | By completing this lab, you have demonstrated that you can create a command line tool using the Rust programming language. You have also demonstrated that you can use the `Clap` framework to parse command line arguments and the Rust standard library to read files and directories. 41 | -------------------------------------------------------------------------------- /resources/banner.svg: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | 9 | 10 | Duke University 11 | 12 | 13 | 14 | 15 | Python and Rust with Linux Command Line Tools 16 | 17 | 18 | 19 | coursera 20 | 21 | 22 | -------------------------------------------------------------------------------- /resources/releasing.md: -------------------------------------------------------------------------------- 1 | ## Releasing 2 | Releasing a package (either a library or a CLI) is usually done with [crates.io](https://crates.io) as the destination. Rust has all the tooling in place via `cargo` to help out with this task. 3 | 4 | The tooling for publishing has several advantages from other programming languages, but mainly, the simplicity and focus helps accomplish publishing without much problems. 5 | 6 | For example, this is a _"dry run"_ (does not actually publish anything) way to try out with `cargo`: 7 | 8 | ``` 9 | $ cargo publish --dry-run 10 | Updating crates.io index 11 | warning: manifest has no description, license, license-file, documentation, homepage or repository. 12 | See https://doc.rust-lang.org/cargo/reference/manifest.html#package-metadata for more info. 13 | error: 5 files in the working directory contain changes that were not yet committed into git: 14 | 15 | Cargo.toml 16 | src/lib.rs 17 | src/main.rs 18 | .vscode/settings.json 19 | src/util.rs 20 | 21 | to proceed despite this and include the uncommitted changes, pass the `--allow-dirty` flag 22 | ``` 23 | 24 | It didn't let me publish without some useful files like the license and the documentation. The amount of extra information, alternatives, and suggestions makes this process straightforward. 25 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | use std::process::Command; 2 | 3 | fn run_command(command: &str) -> String { 4 | let args: Vec<&str> = command.split(" ").collect(); 5 | let output = Command::new(args[0]) 6 | .args(&args[1..]) 7 | .output(); 8 | match output { 9 | Ok(output) => { 10 | let stdout = String::from_utf8_lossy(&output.stdout); 11 | stdout.to_string() 12 | }, 13 | Err(error) => { 14 | println!("Command failed: {command}"); 15 | eprintln!("error: {}", error); 16 | "".to_string() 17 | } 18 | } 19 | 20 | } 21 | 22 | pub fn run_lsblk(device: &str) -> serde_json::Value { 23 | let command = "llsblk -J -o NAME,SIZE,TYPE,MOUNTPOINT"; 24 | let output = run_command(command); 25 | if output.is_empty() { 26 | return serde_json::json!({}); 27 | } 28 | let devices: serde_json::Value = serde_json::from_str(&output).unwrap(); 29 | let devices = devices["blockdevices"].as_array().unwrap(); 30 | for parent in devices { 31 | if parent["name"] == device { 32 | return parent.clone(); 33 | } 34 | if let Some(children) = parent["children"].as_array() { 35 | for child in children { 36 | if child["name"] == device { 37 | return child.clone(); 38 | } 39 | } 40 | } 41 | } 42 | serde_json::json!({}) 43 | } 44 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use clap::{Command, Arg}; 2 | use clap::ColorChoice; 3 | use blkrs::run_lsblk; 4 | 5 | fn main() { 6 | let matches = Command::new("lsblk") 7 | .version("0.0.1") 8 | .author("Alfredo Deza") 9 | .about("lsblk in Rust") 10 | .color(ColorChoice::Always) 11 | .arg( 12 | Arg::new("device") 13 | .help("Device to query") 14 | .required(true) 15 | .index(1) 16 | ) 17 | .get_matches(); 18 | 19 | if let Some(device) = matches.get_one::("device") { 20 | let output = serde_json::to_string(&run_lsblk(&device)).unwrap(); 21 | println!("{}", output); 22 | } else { 23 | println!("No device provided"); 24 | } 25 | } 26 | --------------------------------------------------------------------------------