~elchenberg/not-envsubst

f4ab9be4f33746392bb68bbc614cd1283a7dc474 — elchenberg 18 days ago main
Initial commit
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
}