A => .editorconfig +15 -0
@@ 1,15 @@
+root = true
+
+[*]
+indent_style = space
+indent_size = 2
+end_of_line = lf
+trim_trailing_spaces = true
+insert_final_newline = true
+
+[*.md]
+trim_trailing_spaces = false
+max_line_length = 80
+
+[{*.rs,Dockerfile}]
+indent_size = 4
A => .gitignore +14 -0
@@ 1,14 @@
+
+# Created by https://www.toptal.com/developers/gitignore/api/rust
+# Edit at https://www.toptal.com/developers/gitignore?templates=rust
+
+### Rust ###
+# Generated by Cargo
+# will have compiled files and executables
+/target/
+
+# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries
+# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html
+# Cargo.lock
+
+# End of https://www.toptal.com/developers/gitignore/api/rust
A => .rustfmt.toml +1 -0
@@ 1,1 @@
+edition = "2018"
A => Cargo.lock +113 -0
@@ 1,113 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+[[package]]
+name = "aho-corasick"
+version = "0.7.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7404febffaa47dac81aa44dba71523c9d069b1bdc50a77db41195149e17f68e5"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "gumdrop"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "46571f5d540478cf70d2a42dd0d6d8e9f4b9cc7531544b93311e657b86568a0b"
+dependencies = [
+ "gumdrop_derive",
+]
+
+[[package]]
+name = "gumdrop_derive"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "915ef07c710d84733522461de2a734d4d62a3fd39a4d4f404c2f385ef8618d05"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "lazy_static"
+version = "1.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
+
+[[package]]
+name = "memchr"
+version = "2.3.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0ee1c47aaa256ecabcaea351eae4a9b01ef39ed810004e298d2511ed284b1525"
+
+[[package]]
+name = "not-envsubst"
+version = "0.1.0"
+dependencies = [
+ "gumdrop",
+ "lazy_static",
+ "regex",
+]
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.24"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e0704ee1a7e00d7bb417d0770ea303c1bccbabf0ef1667dae92b5967f5f8a71"
+dependencies = [
+ "unicode-xid",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "991431c3519a3f36861882da93630ce66b52918dcf1b8e2fd66b397fc96f28df"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "regex"
+version = "1.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "38cf2c13ed4745de91a5eb834e11c00bcc3709e773173b2ce4c56c9fbde04b9c"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-syntax",
+ "thread_local",
+]
+
+[[package]]
+name = "regex-syntax"
+version = "0.6.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3b181ba2dcf07aaccad5448e8ead58db5b742cf85dfe035e2227f137a539a189"
+
+[[package]]
+name = "syn"
+version = "1.0.55"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a571a711dddd09019ccc628e1b17fe87c59b09d513c06c026877aa708334f37a"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-xid",
+]
+
+[[package]]
+name = "thread_local"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d40c6d1b69745a6ec6fb1ca717914848da4b44ae29d9b3080cbee91d72a69b14"
+dependencies = [
+ "lazy_static",
+]
+
+[[package]]
+name = "unicode-xid"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f7fe0bb3479651439c9112f72b6c505038574c9fbb575ed1bf3b797fa39dd564"
A => Cargo.toml +25 -0
@@ 1,25 @@
+[package]
+name = "not-envsubst"
+description = "Substitutes the values of environment variables."
+version = "0.1.0"
+edition = "2018"
+
+[[bin]]
+name = "not-envsubst"
+path = "src/main.rs"
+
+[dependencies]
+gumdrop = "0.8.0"
+lazy_static = "1.4.0"
+# Regex with default features results in a 15.1 MiB binary.
+regex = "1.4.2"
+
+# Regex without default features results in a 9.9 MiB binary.
+#[dependencies.regex]
+#version = "1.4.2"
+#default-features = false
+#features = ["std"]
+
+[profile.release]
+lto = true
+codegen-units = 1
A => LICENSE.txt +21 -0
@@ 1,21 @@
+MIT License
+
+Copyright (c) 2020 Helge Eichelberg
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
A => README.md +27 -0
@@ 1,27 @@
+# not-envsubst (working title)
+
+## Goal
+
+1. Learn Rust
+2. Create a replacement for [envsubst](https://www.gnu.org/software/gettext/manual/html_node/envsubst-Invocation.html) that supports:
+ - `$VAR`
+ - `${VAR}`
+ - `${VAR-default}`
+ - `${VAR:-default}`
+ - `${VAR?message}`
+ - and `${VAR:?message}`
+
+## Usage
+
+```
+not-envsubst v0.1.0
+
+Substitutes the values of environment variables.
+
+Usage:
+ not-envsubst [OPTIONS]
+
+Optional arguments:
+ -h, --help print this help message
+ -V, --version print the version number
+```
A => TODO.md +15 -0
@@ 1,15 @@
+# TODO
+
+- [x] support `$parameter` variables
+- [ ] add tests
+- [ ] add benchmarks
+- [ ] support `${parameter}` variables
+- [ ] support escaped variables, e. g. `\${parameter}` → `${parameter}` and `\$parameter` → `$parameter`
+- [ ] support `${parameter-default}` and `${parameter:-default}` variables
+- [ ] support `${parameter?err_msg}` and `${parameter:?err_msg}` variables
+- [ ] add a `-v, --variables` flag (envsubst drop-in replacement)
+- [ ] support `${parameter+alt_value}` and `${parameter:+alt_value}` variables
+- [ ] add continuous integration
+- [ ] add continuous deployment (or delivery)
+- [ ] …
+- [ ] release v1.0.0
A => docker-compose.yml +16 -0
@@ 1,16 @@
+version: "3.8"
+services:
+ rust:
+ image: rust:1.48.0-buster
+ cap_drop: [ ALL ]
+ read_only: true
+ tmpfs: /tmp
+ command: cargo install --path .
+ volumes:
+ - .:/workdir:rw
+ - cargo-data:/usr/local/cargo
+ - rustup-data:/usr/local/rustup
+ working_dir: /workdir
+volumes:
+ cargo-data:
+ rustup-data:
A => src/cli.rs +61 -0
@@ 1,61 @@
+use std::env::args;
+use std::process::exit;
+
+use gumdrop::Options;
+
+#[derive(Options)]
+pub struct Args {
+ #[options(help = "print this help message")]
+ pub help: bool,
+
+ #[options(help = "print the version number", short = "V")]
+ pub version: bool,
+}
+
+pub fn get_args() -> Args {
+ let env_args = args().collect::<Vec<_>>();
+ match Args::parse_args_default(&env_args[1..]) {
+ Ok(args) => args,
+ Err(error) => {
+ eprintln!("ERROR: {}", error);
+ eprintln!();
+ eprintln!("{}", get_help());
+ exit(1);
+ }
+ }
+}
+
+pub fn print_help_and_exit() {
+ print_help();
+ exit(0);
+}
+
+pub fn print_version_and_exit() {
+ print_version();
+ exit(0);
+}
+
+fn get_help() -> String {
+ format!(
+ "{name} v{version}\n\n{description}\n\nUsage:\n {bin_name} [OPTIONS]\n\n{usage}",
+ name = crate::pkg::get_name(),
+ version = crate::pkg::get_version(),
+ description = crate::pkg::get_description(),
+ bin_name = args()
+ .next()
+ .unwrap_or_else(|| crate::pkg::get_bin_name().to_string()),
+ usage = get_usage(),
+ )
+}
+
+fn print_version() {
+ println!("{}", crate::pkg::get_version());
+}
+
+fn print_help() {
+ println!("{}", get_help())
+}
+
+fn get_usage() -> &'static str {
+ Args::usage()
+}
A => src/main.rs +26 -0
@@ 1,26 @@
+#![forbid(unsafe_code)]
+#![warn(clippy::all)]
+#![warn(clippy::pedantic)]
+
+mod cli;
+mod tpl;
+mod pkg;
+mod stdin;
+
+fn main() {
+ let args = crate::cli::get_args();
+
+ if args.help {
+ crate::cli::print_help_and_exit();
+ }
+
+ if args.version {
+ crate::cli::print_version_and_exit();
+ }
+
+ let input: Vec<String> = crate::stdin::get_lines_or_exit();
+ let output: Vec<String> = crate::tpl::replace_variables(input);
+ for line in output {
+ print!("{}", line);
+ }
+}
A => src/pkg.rs +15 -0
@@ 1,15 @@
+pub fn get_bin_name() -> &'static str {
+ env!("CARGO_BIN_NAME")
+}
+
+pub fn get_description() -> &'static str {
+ env!("CARGO_PKG_DESCRIPTION")
+}
+
+pub fn get_name() -> &'static str {
+ env!("CARGO_PKG_NAME")
+}
+
+pub fn get_version() -> &'static str {
+ env!("CARGO_PKG_VERSION")
+}
A => src/stdin.rs +44 -0
@@ 1,44 @@
+use std::io::stdin;
+use std::process::exit;
+use std::sync::mpsc::channel;
+use std::sync::mpsc::Receiver;
+use std::sync::mpsc::TryRecvError;
+use std::thread::sleep;
+use std::thread::spawn;
+use std::time::Duration;
+
+pub fn get_lines_or_exit() -> Vec<String> {
+ match get_lines() {
+ Ok(lines) => lines,
+ Err(TryRecvError::Empty) => {
+ eprintln!("ERROR: Please provide some input via STDIN");
+ exit(1)
+ }
+ Err(error) => {
+ eprintln!("ERROR: {}", error);
+ exit(1)
+ }
+ }
+}
+
+pub fn get_lines() -> Result<Vec<String>, TryRecvError> {
+ let stdin_channel = spawn_stdin_channel();
+ sleep(Duration::from_millis(10));
+ stdin_channel.try_recv()
+}
+
+// Spawning a channel is a workaround that allows us to catch missing STDIN input with a TryRecvError.
+fn spawn_stdin_channel() -> Receiver<Vec<String>> {
+ let (sender, receiver) = channel::<Vec<String>>();
+ spawn(move || {
+ let mut input_lines: Vec<String> = Vec::new();
+ let mut line = String::new();
+ let stdin = stdin();
+ while stdin.read_line(&mut line).unwrap() > 0 {
+ input_lines.push(line.clone());
+ line.clear();
+ }
+ sender.send(input_lines).unwrap();
+ });
+ receiver
+}
A => src/tpl.rs +26 -0
@@ 1,26 @@
+use std::env::var;
+
+use lazy_static::lazy_static;
+use regex::Regex;
+
+lazy_static! {
+ static ref PARAMETERS_REGEX: Regex = Regex::new(r"(?x)
+ \$(?P<name>\w[\w\d_]*) # capture the name of parameters without curly braces, eg. $USER
+ ").unwrap();
+}
+
+pub fn replace_variables(input: Vec<String>) -> Vec<String> {
+ let mut output: Vec<String> = Vec::new();
+ for line in input {
+ let mut clone = line.clone();
+ if line.find('$').is_some() {
+ for captures in PARAMETERS_REGEX.captures_iter(&line) {
+ let name = captures["name"].to_string();
+ let value = var(&name).unwrap_or_else(|_| String::from(""));
+ clone = clone.replace(&captures[0], &value);
+ }
+ }
+ output.push(clone);
+ }
+ output
+}