-
Notifications
You must be signed in to change notification settings - Fork 1
7. Project structure
Here you'll find some information about our code structure and how to add new feature.
.
├── assets # Assets directory
│ └── default_config.toml # Default configuration file
├── install.ps1 # Windows install script
├── install.sh # Unix install script
├── src
│ ├── commands.rs # Commands handlers
│ ├── config.rs # Configuration support
│ ├── interactive
│ │ ├── draw.rs # Draw interactive mode
│ │ ├── list.rs # List environment variables
│ │ └── tests.rs # Tests for interactive mode
│ ├── interactive.rs # Main interactive mode handler
│ ├── main.rs # App's entrypoint
│ ├── models.rs # Models and structures
│ ├── utils.rs # Useful utilities
│ └── variables.rs # Working with variables
└── tests # Integration tests
└── cli.rs # Integration tests for CLI
Scoop is a package manager for Windows. Our manifest for Scoop is publishhed as GitHub Gist in my account. Link.
You can find some URLs to this Gist:
- Gist link: https://gist.github.com/ankddev/f6314b552aa021f676fc999ec697f833
- Raw link to specific iteration: https://gist.github.com/ankddev/f6314b552aa021f676fc999ec697f833/raw/86e564bc9803e7d30e51ad7edc2d665340b324e9/envfetch.json
- And raw link for latest iteration: https://gist.github.com/ankddev/f6314b552aa021f676fc999ec697f833/raw/envfetch.json For installing you should use raw link to gist. While you can use specific iteration, version can be outdated, so I recommend you to use raw link without specific commit.
Install scripts are used to easily install envfetch
on different platforms.
This is just PowerShell script, that downloads and extracts to specific directory executable form releases. Actual source of this script. Firstly, we create directory, where program will be installed and go to it:
$envfetchPath = "$env:APPDATA\envfetch" # Define directory path
New-Item -ItemType Directory -Force -Path $envfetchPath -ErrorAction SilentlyContinue | Out-Null # Create directory
Write-Host "Installing envfetch to $envfetchPath" # Print message with path of directory
Push-Location $envfetchPath # Go to this directory, as cd
Then, there are some code that can crash, so it is in try
block with finally
, where we go back to directory, where user did before:
try {
...
} finally {
Pop-Location # Go back to directory
}
In next snippet we:
- Check if file alreday exists and if yes, then print message about updating
- Download file to installation directory
- Compare checksums
- Add this directory to
PATH
, so user can easily access it
$outFile = "envfetch.exe"
if ([System.IO.File]::Exists($outFile)) {
Write-Host "envfetch already exists. Updating..."
} # Define output path
Invoke-WebRequest -Uri "https://github.com/ankddev/envfetch/releases/latest/download/envfetch-windows-amd64.exe" -OutFile $outFile # Download file from release
# Check integrity
$expectedChecksum = Invoke-WebRequest -Uri "https://github.com/ankddev/envfetch/releases/latest/download/envfetch-windows-amd64.exe.sha256"
$expectedChecksum = [System.Text.Encoding]::UTF8.GetString($expectedChecksum.Content).TrimEnd()
$actualChecksum = (Get-FileHash -Path $outFile -Algorithm SHA256).Hash.ToLower()
if ($actualChecksum -ne $expectedChecksum) {
Write-Host "Checksum mismatch!"
Write-Host "Expected: \"$expectedChecksum\""
Write-Host "Actual: \"$actualChecksum\""
Write-Host "Please download the file manually and verify its integrity."
exit 1
}
# Add directory to PATH
$userPath = [Environment]::GetEnvironmentVariable("Path", "User")
if ($userPath -notlike "*$envfetchPath*") {
[Environment]::SetEnvironmentVariable("Path", "$userPath;$envfetchPath", "User")
}
And, finally, we print message that program installed successfully:
Write-Host "envfetch has been installed and added to your PATH. Please restart your terminal for the changes to take effect."
This is Bash script, that downloads and extracts to specific directory executable form releases. Actual source of this script. Firstly, we run following command to exit immediately on error:
set -e
Then, we define function to calculate file's checksums:
# Calculate checksum. We use this function because Linux and macOS have different commands
calculate_checksum() {
if [ "$OS_NAME" = "Darwin" ]; then
# macOS
shasum -a 256 "$1" | cut -d ' ' -f 1
else
# Linux and others
sha256sum "$1" | cut -d ' ' -f 1
fi
}
Then, we define some variables, we'll use later:
OS_NAME=$(uname -s) # Name of OS
ARCH_NAME=$(uname -m) # Architecture name
OS="" # Actual OS
ARCH="" # Actual arch
INSTALL_DIR="/usr/local/bin" # Install directory path
NO_RELEASE_ASSET="" # Helper variable that shows that release don't found
Then, we print path to user and activate sudo (currently install script installs only system-wide):
echo "Installing envfetch to $INSTALL_DIR"
echo "This script will activate sudo to install to $INSTALL_DIR"
sudo echo "Sudo activated"
Then we have large snippet that gets user's OS and arch:
if [ "$OS_NAME" = "Linux" ]; then
OS="Linux"
if [ "$ARCH_NAME" = "x86_64" ]; then
BUILD_TARGET="linux-amd64"
elif [ "$ARCH_NAME" = "arm" ] || [ "$ARCH_NAME" = "arm64" ]; then
BUILD_TARGET="linux-arm64"
else
NO_RELEASE_ASSET="true"
echo "There is no release for this architecture: $ARCH_NAME" >&2
fi
elif [ "$OS_NAME" = "Darwin" ]; then
OS="macOS"
if [ "$ARCH_NAME" = "x86_64" ]; then
BUILD_TARGET="darwin-amd64"
elif [ "$ARCH_NAME" = "arm" ] || [ "$ARCH_NAME" = "arm64" ]; then
BUILD_TARGET="darwin-arm64"
else
NO_RELEASE_ASSET="true"
echo "There is no release for this architecture: $ARCH_NAME" >&2
fi
else
NO_RELEASE_ASSET="true"
echo "There is no Unix release for this OS: $OS_NAME" >&2
fi
How it works? Firstly we check, if OS is Linux. If not, check, maybe it is Darwin (macOS). If not, then we set NO_RELEASE_ASSET
to true, If OS is Linux or macOS, then we check architecture. And, finally, set neccessary build target.
If no release target found, we exit with 1 code:
if [ "$NO_RELEASE_ASSET" ]; then
exit 1 # Exit with non-zero exit code to indicate, that there is an error
fi
Then, check if file already exists and print message about updating:
if [ -f $INSTALL_DIR/envfetch ]; then
echo "envfetch is already installed. Updating..."
fi
Then, we download executable from release to installation directory:
# Download file directly to install directory and give it permissions to execute:
sudo curl -sSL "https://github.com/ankddev/envfetch/releases/latest/download/envfetch-$BUILD_TARGET" --output "$INSTALL_DIR/envfetch"
# Give permissions for executable
sudo chmod +x "$INSTALL_DIR/envfetch"
After that we compare checksums:
# Check integrity
EXPECTED_CHECKSUM=$(curl -sSL "https://github.com/ankddev/envfetch/releases/latest/download/envfetch-$BUILD_TARGET.sha256" | tr -d '[:space:]' | tr -d '\n')
ACTUAL_CHECKSUM=$(calculate_checksum "$INSTALL_DIR/envfetch" | awk '{print $1}')
if [ "$EXPECTED_CHECKSUM" != "$ACTUAL_CHECKSUM" ]; then
echo "Checksum mismatch" >&2
echo "Expected: $EXPECTED_CHECKSUM"
echo "Actual: $ACTUAL_CHECKSUM"
echo "Please try again later or report this issue to the developer."
exit 1
fi
And, finally, we print message about successfull installation:
echo "Successfully installed envfetch"
Every command has some structure. Let's go through it!
In models.rs we have struct with some CLI arguments:
#[derive(Parser)]
#[command(
author,
version,
after_help = "Get more info at project's repo: https://github.com/ankddev/envfetch",
after_long_help = "Get more info at project's GitHub repo available at https://github.com/ankddev/envfetch",
arg_required_else_help = true,
name = "envfetch"
)]
#[command(
about = "envfetch - lightweight tool for working with environment variables",
long_about = "envfetch is a lightweight cross-platform CLI tool for working with environment variables"
)]
pub struct Cli {
/// Tool commands
#[command(subcommand)]
pub command: Commands,
}
It contains only one field - command, which is Comamnds
enum. Let's look in it!
/// All tool's commands
#[derive(Subcommand, Debug, PartialEq, Eq)]
pub enum Commands {
/// Open envfetch in interactive mode with TUI.
Interactive,
/// Print value of environment variable.
Get(GetArgs),
/// Set environment variable and optionally run given process.
Set(SetArgs),
/// Add value to the end of environment variable and optionally run given process.
Add(AddArgs),
/// Delete environment variable and optionally run given process.
Delete(DeleteArgs),
/// Load environment variables from dotenv file and optionally run given process.
Load(LoadArgs),
/// Print all environment variables.
Print(PrintArgs),
/// Initialize config file.
InitConfig,
}
We have several derive
's, but they don't wonder as. Here we have list of all envfetch
's commands. Note that doccomment on this enum's element is description in help menu (clap
will generate it for us), so always write it. Some of commands have arguments. For example, let's look to args for set
command (SetArgs
struct):
/// Args for set command
#[derive(Args, Debug, PartialEq, Eq)]
pub struct SetArgs {
/// Environment variable name
#[arg(required = true)]
pub key: String,
/// Value for environment variable
#[arg(required = true)]
pub value: String,
/// Globally set variable
#[arg(required = false, long, short)]
pub global: bool,
/// Process to start, not required if --global flag is set
#[arg(last = true, required_unless_present = "global")]
pub process: Option<String>,
}
Comments on fields mean descriptions of argumnets. Here we see some arguments:
- Fisrt is key. It's type is
String
, it's marked as required and don't havelong
orshort
, so it is positional. - Second is value. It also has type
String
, also required and positional. - Then we have global. It has type
bool
. It's marked as not required and haslong
andshort
(they aren't set, becauseclap
automatically use field's name as long and first letter as shhort, but we can override it, e.g.long = "helloarg"
), so it's optional flag. - And we have process arg. Firstly, it isn't required if global flag is present so has type
Option<String>
(you can notice that gloabl also optional, but don't have typeOption
. That's because if it's not present,false
is automatically used). But what aboutlast = true
? This means that this arg is last argument - all content after--
will be assigned to it, e.g. withenvfetch set hello hello -- npm run boot
valuenpm run boot
will be assigned to this field
Then, we have run_command()
function in commands.rs that is runned from main()
function.
It's signature is:
pub fn run_command<W: Write>(
command: &Commands,
config: Option<Config>,
mut buffer: W,
) -> ExitCode {
It accepts Commands
enum with current command, optional config and.. buffer. Yes, we need buffer for testing purposes. If we will just use println!()
macro everywhere, we won't test it, because we can't just read stdout
or stderr
. And it returns ExitCode
to indicate, is program finished successfully. This functions matches command
and run needed function.
Then we have functions for specific commands. Here's code for set()
function:
/// Set value to environment variable
pub fn set(args: &SetArgs) -> Result<(), ErrorKind> {
validate_var_name(&args.key).map_err(ErrorKind::NameValidationError)?;
variables::set_variable(&args.key, &args.value, args.global, args.process.clone())?;
Ok(())
}
It validates name of variable and then.. runs another function. Why? This allows to reuse some logic to avoid duplication.
Then, in variables.rs we have functions for working with variables directly. Let's check set_variable()
function:
/// Set variable with given key and value
pub fn set_variable(
key: &str,
value: &str,
global: bool,
process: Option<String>,
) -> Result<(), ErrorKind> {
if global {
if let Err(err) = globalenv::set_var(key, value) {
return Err(ErrorKind::CannotSetVariableGlobally(err.to_string()));
}
} else {
unsafe { env::set_var(key, value) };
}
if let Some(process) = process {
return run(process, false);
}
Ok(())
}
It checks thhat we want to set variable locally or globally, sets it with specific function and than, if needed, runs specified process.