feat: Display full recipe with syntax highlighting (#166)

As I was re-arranging my recipe files, I needed a way to ensure that the
order of my recipe is correct without having to read through the
generated `Containerfile`. So I added a `-d`/`--display-full-recipe` arg
to `template` that will print out all of your modules in the order
defined by following the `from-file` property.

```
$> bluebuild template --help
Generate a Containerfile from a recipe

Usage: bluebuild template [OPTIONS] [RECIPE]

Arguments:
  [RECIPE]
          The recipe file to create a template from

Options:
  -o, --output <OUTPUT>
          File to output to instead of STDOUT

      --registry <REGISTRY>
          The registry domain the image will be published to.

          This is used for modules that need to know where the image is being published (i.e. the signing module).

      --registry-namespace <REGISTRY_NAMESPACE>
          The registry namespace the image will be published to.

          This is used for modules that need to know where the image is being published (i.e. the signing module).

  -d, --display-full-recipe
          Instead of creating a Containerfile, display the full recipe after traversing all `from-file` properties.

          This can be used to help debug the order you defined your recipe.

  -t, --syntax-theme <SYNTAX_THEME>
          Choose a theme for the syntax highlighting for the Containerfile or Yaml.

          The default is `mocha-dark`.

          [possible values: mocha-dark, ocean-dark, ocean-light, eighties-dark, inspired-github, solarized-dark, solarized-light]

  -s, --squash
          Puts the build in a `squash-stage` and COPY's the results to the final stage as one layer.

          WARN: This doesn't work with the docker driver as it has been deprecated.

          NOTE: Squash has a performance benefit for the newer versions of podman and buildah.

  -B, --build-driver <BUILD_DRIVER>
          Select which driver to use to build your image

          [possible values: buildah, podman, docker]

  -v, --verbose...
          Increase logging verbosity

  -I, --inspect-driver <INSPECT_DRIVER>
          Select which driver to use to inspect images

          [possible values: skopeo, podman, docker]

  -q, --quiet...
          Decrease logging verbosity

  -h, --help
          Print help (see a summary with '-h')
```

Preview of Containerfile/Dockerfile syntax highlighting:

![image](https://github.com/blue-build/cli/assets/4626052/cf2c452e-94b1-44f3-97ca-162d50a5047f)

Preview of Yaml highlighting:

![image](https://github.com/blue-build/cli/assets/4626052/b7c48b82-3e9e-431c-a55b-679848cd1fa6)
This commit is contained in:
Gerald Pinder 2024-04-27 09:12:04 -04:00 committed by GitHub
parent a7503d561e
commit 92150693d4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 477 additions and 9 deletions

View file

@ -9,8 +9,10 @@ license.workspace = true
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
atty = "0.2.14"
directories = "5"
process_control = { version = "4.0.3", features = ["crossbeam-channel"] }
syntect = "5.2.0"
which = "6"
anyhow.workspace = true
@ -23,6 +25,13 @@ serde.workspace = true
serde_yaml.workspace = true
serde_json.workspace = true
[dependencies.clap]
workspace = true
features = ["derive"]
[build-dependencies]
syntect = "5.2.0"
[lints]
workspace = true

24
utils/build.rs Normal file
View file

@ -0,0 +1,24 @@
use std::env;
use std::path::PathBuf;
use syntect::dumps;
use syntect::parsing::syntax_definition::SyntaxDefinition;
use syntect::parsing::SyntaxSetBuilder;
fn main() {
let mut ssb = SyntaxSetBuilder::new();
ssb.add(
SyntaxDefinition::load_from_str(
include_str!("highlights/Dockerfile.sublime-syntax"),
true,
None,
)
.unwrap(),
);
let ss = ssb.build();
dumps::dump_to_uncompressed_file(
&ss,
PathBuf::from(env::var("OUT_DIR").unwrap()).join("docker_syntax.bin"),
)
.unwrap();
}

View file

@ -0,0 +1,141 @@
%YAML 1.2
# The MIT License (MIT)
#
# Copyright 2014 Asbjorn Enge
#
# 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.
# https://github.com/asbjornenge/Docker.tmbundle
---
# http://www.sublimetext.com/docs/3/syntax.html
name: Dockerfile
scope: source.dockerfile
file_extensions:
- Dockerfile
- dockerfile
hidden_file_extensions:
- .Dockerfile
first_line_match: ^\s*(?i:(from(?!\s+\S+\s+import)|arg))\s+
variables:
onbuild_directive: (?i:(onbuild)\s+)?
onbuild_commands_directive:
"{{onbuild_directive}}(?i:add|arg|env|expose|healthcheck|label|run|shell|stopsignal|user|volume|workdir)"
nononbuild_commands_directive: (?i:maintainer)
runtime_directive: "{{onbuild_directive}}(?i:cmd|entrypoint)"
from_directive: (?i:(from))\s+[^\s:@]+(?:[:@](\S+))?(?:\s+(?i:(as))\s+(\S+))?
copy_directive: ({{onbuild_directive}}(?i:copy))(?:\s+--from=(\S+))?
contexts:
main:
- include: comments
- match: ^(?i:arg)\s
scope: keyword.control.dockerfile
- include: from
from:
- match: ^{{from_directive}}
captures:
1: keyword.control.dockerfile
2: entity.name.enum.tag-digest
3: keyword.control.dockerfile
4: variable.stage-name
push: body
body:
- include: comments
- include: directives
- include: invalid
- include: from
directives:
- match: ^\s*{{onbuild_commands_directive}}\s
captures:
0: keyword.control.dockerfile
1: keyword.other.special-method.dockerfile
push: args
- match: ^\s*{{nononbuild_commands_directive}}\s
scope: keyword.control.dockerfile
push: args
- match: ^\s*{{copy_directive}}\s
captures:
1: keyword.control.dockerfile
2: keyword.other.special-method.dockerfile
3: variable.stage-name
push: args
- match: ^\s*{{runtime_directive}}\s
captures:
0: keyword.operator.dockerfile
1: keyword.other.special-method.dockerfile
push: args
escaped-char:
- match: \\.
scope: constant.character.escaped.dockerfile
args:
- include: comments
- include: escaped-char
- match: ^\s*$
- match: \\\s+$
- match: \n
pop: true
- match: '"'
scope: punctuation.definition.string.begin.dockerfile
push: double_quote_string
- match: "'"
scope: punctuation.definition.string.begin.dockerfile
push: single_quote_string
double_quote_string:
- meta_scope: string.quoted.double.dockerfile
- include: escaped-char
- match: ^\s*$
- match: \\\s+$
- match: \n
set: invalid
- match: '"'
scope: punctuation.definition.string.end.dockerfile
pop: true
single_quote_string:
- meta_scope: string.quoted.single.dockerfile
- include: escaped-char
- match: ^\s*$
- match: \\\s+$
- match: \n
set: invalid
- match: "'"
scope: punctuation.definition.string.end.dockerfile
pop: true
comments:
- match: ^(\s*)((#).*$\n?)
comment: comment.line
captures:
1: punctuation.whitespace.comment.leading.dockerfile
2: comment.dockerfile
3: punctuation.definition.comment.dockerfile
invalid:
- match: ^[^A-Z\n](.*)$
scope: invalid
set: body

View file

@ -1,6 +1,7 @@
pub mod command_output;
pub mod constants;
pub mod logging;
pub mod syntax_highlighting;
use std::{ffi::OsStr, io::Write, path::PathBuf, process::Command, thread, time::Duration};

View file

@ -0,0 +1,85 @@
use anyhow::{anyhow, Result};
use clap::ValueEnum;
use log::trace;
use serde::ser::Serialize;
use syntect::{dumps, easy::HighlightLines, highlighting::ThemeSet, parsing::SyntaxSet};
#[derive(Debug, Default, Clone, Copy, ValueEnum)]
pub enum DefaultThemes {
#[default]
MochaDark,
OceanDark,
OceanLight,
EightiesDark,
InspiredGithub,
SolarizedDark,
SolarizedLight,
}
impl std::fmt::Display for DefaultThemes {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(match *self {
Self::MochaDark => "base16-mocha.dark",
Self::OceanDark => "base16-ocean.dark",
Self::OceanLight => "base16-ocean.light",
Self::EightiesDark => "base16-eighties.dark",
Self::InspiredGithub => "InspiredGithub",
Self::SolarizedDark => "Solarized (dark)",
Self::SolarizedLight => "Solarized (light)",
})
}
}
/// Prints the file with syntax highlighting.
///
/// # Errors
/// Will error if the theme doesn't exist, the syntax doesn't exist, or the file
/// failed to serialize.
pub fn print(file: &str, file_type: &str, theme: Option<DefaultThemes>) -> Result<()> {
trace!("syntax_highlighting::print({file}, {file_type}, {theme:?})");
if atty::is(atty::Stream::Stdout) {
let ss: SyntaxSet = if file_type == "dockerfile" || file_type == "Dockerfile" {
dumps::from_uncompressed_data(include_bytes!(concat!(
env!("OUT_DIR"),
"/docker_syntax.bin"
)))?
} else {
SyntaxSet::load_defaults_newlines()
};
let ts = ThemeSet::load_defaults();
let syntax = ss
.find_syntax_by_extension(file_type)
.ok_or_else(|| anyhow!("Failed to get syntax"))?;
let mut h = HighlightLines::new(
syntax,
ts.themes
.get(theme.unwrap_or_default().to_string().as_str())
.ok_or_else(|| anyhow!("Failed to get highlight theme"))?,
);
for line in file.lines() {
let ranges = h.highlight_line(line, &ss)?;
let escaped = syntect::util::as_24_bit_terminal_escaped(&ranges, false);
println!("{escaped}");
}
println!("\x1b[0m");
} else {
println!("{file}");
}
Ok(())
}
/// Takes a serializable struct and prints it out with syntax highlighting.
///
/// # Errors
/// Will error if the theme doesn't exist, the syntax doesn't exist, or the file
/// failed to serialize.
pub fn print_ser<T: Serialize>(
file: &T,
file_type: &str,
theme: Option<DefaultThemes>,
) -> Result<()> {
print(serde_yaml::to_string(file)?.as_str(), file_type, theme)?;
Ok(())
}