Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement builtin glob function #488

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions src/modules/expression/expr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ use super::typeop::{
use super::parentheses::Parentheses;
use crate::modules::variable::get::VariableGet;
use super::ternop::ternary::Ternary;
use crate::modules::function::glob::GlobInvocation;
use crate::modules::function::invocation::FunctionInvocation;
use crate::modules::builtin::nameof::Nameof;
use crate::{document_expression, parse_expr, parse_expr_group, translate_expression};
Expand Down Expand Up @@ -71,6 +72,7 @@ pub enum ExprType {
Neq(Neq),
Not(Not),
Ternary(Ternary),
GlobInvocation(GlobInvocation),
FunctionInvocation(FunctionInvocation),
Command(Command),
Array(Array),
Expand Down Expand Up @@ -148,6 +150,8 @@ impl SyntaxModule<ParserMetadata> for Expr {
// Literals
Parentheses, Bool, Number, Text,
Array, Null, Nameof, Status,
// Builtin invocation
GlobInvocation,
// Function invocation
FunctionInvocation, Command,
// Variable access
Expand Down Expand Up @@ -176,12 +180,21 @@ impl TranslateModule for Expr {
Not, Neg, Nameof,
// Literals
Parentheses, Bool, Number, Text, Array, Null, Status,
// Builtin invocation
GlobInvocation,
// Function invocation
FunctionInvocation, Command,
// Variable access
VariableGet
])
}

fn conditional(&self, name: &str) -> Option<String> {
match &self.value {
Some(ExprType::GlobInvocation(value)) => value.conditional(name),
_ => None,
}
}
}

impl DocumentationModule for Expr {
Expand All @@ -201,6 +214,8 @@ impl DocumentationModule for Expr {
Not, Neg, Nameof,
// Literals
Parentheses, Bool, Number, Text, Array, Null, Status,
// Builtin invocation
GlobInvocation,
// Function invocation
FunctionInvocation, Command,
// Variable access
Expand Down
12 changes: 10 additions & 2 deletions src/modules/expression/literal/text.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ use super::{parse_interpolated_region, translate_interpolated_region};
pub struct Text {
strings: Vec<String>,
interps: Vec<Expr>,
escaped: bool,
}

impl Typed for Text {
Expand All @@ -24,11 +25,13 @@ impl SyntaxModule<ParserMetadata> for Text {
Text {
strings: vec![],
interps: vec![],
escaped: false,
}
}

fn parse(&mut self, meta: &mut ParserMetadata) -> SyntaxResult {
(self.strings, self.interps) = parse_interpolated_region(meta, '"')?;
self.escaped = meta.context.is_escaped_ctx;
Ok(())
}
}
Expand All @@ -39,8 +42,13 @@ impl TranslateModule for Text {
let interps = self.interps.iter()
.map(|item| item.translate(meta))
.collect::<Vec<String>>();
let quote = meta.gen_quote();
format!("{quote}{}{quote}", translate_interpolated_region(self.strings.clone(), interps, true))
let strings = translate_interpolated_region(self.strings.clone(), interps, true);
if self.escaped {
strings.replace(" ", "\\ ").replace(";", "\\;")
} else {
let quote = meta.gen_quote();
format!("{quote}{strings}{quote}")
}
}
}

Expand Down
68 changes: 68 additions & 0 deletions src/modules/function/glob.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
use std::mem::swap;

use heraclitus_compiler::prelude::*;
use itertools::Itertools;
use crate::docs::module::DocumentationModule;
use crate::modules::expression::expr::Expr;
use crate::modules::types::{Type, Typed};
use crate::translate::module::TranslateModule;
use crate::utils::metadata::{ParserMetadata, TranslateMetadata};

#[derive(Debug, Clone)]
pub struct GlobInvocation {
args: Vec<Expr>,
}

impl Typed for GlobInvocation {
fn get_type(&self) -> Type {
Type::Array(Box::new(Type::Text))
}
}

impl SyntaxModule<ParserMetadata> for GlobInvocation {
syntax_name!("Glob Invocation");

fn new() -> Self {
GlobInvocation {
args: vec![],
}
}

fn parse(&mut self, meta: &mut ParserMetadata) -> SyntaxResult {
token(meta, "glob")?;
token(meta, "(")?;
let mut new_is_escaped_ctx = true;
swap(&mut new_is_escaped_ctx, &mut meta.context.is_escaped_ctx);
loop {
let tok = meta.get_current_token();
let mut arg = Expr::new();
syntax(meta, &mut arg)?;
if arg.get_type() != Type::Text {
return error!(meta, tok, "Expected string");
}
self.args.push(arg);
match token(meta, ")") {
Ok(_) => break,
Err(_) => token(meta, ",")?,
};
}
swap(&mut new_is_escaped_ctx, &mut meta.context.is_escaped_ctx);
Ok(())
}
}

impl TranslateModule for GlobInvocation {
fn translate(&self, meta: &mut TranslateMetadata) -> String {
self.args.iter().map(|arg| arg.translate(meta)).join(" ")
}

fn conditional(&self, name: &str) -> Option<String> {
Some(format!("[ -e \"${{{}}}\" ]", name))
}
}

impl DocumentationModule for GlobInvocation {
fn document(&self, _meta: &ParserMetadata) -> String {
"".to_string()
}
}
1 change: 1 addition & 0 deletions src/modules/function/mod.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
pub mod declaration;
pub mod declaration_utils;
pub mod glob;
pub mod invocation;
pub mod invocation_utils;
pub mod ret;
Expand Down
54 changes: 40 additions & 14 deletions src/modules/loops/iter_loop.rs
Original file line number Diff line number Diff line change
Expand Up @@ -80,22 +80,48 @@ impl TranslateModule for IterLoop {
meta.increase_indent();
let indent = meta.gen_indent();
meta.decrease_indent();
[
format!("{index}=0;"),
format!("for {name} in {expr}"),
"do".to_string(),
self.block.translate(meta),
format!("{indent}(( {index}++ )) || true"),
"done".to_string(),
].join("\n")
if let Some(conditional) = self.iter_expr.conditional(name) {
[
format!("{index}=0;"),
format!("for {name} in {expr}"),
"do".to_string(),
format!("if {conditional}"),
"then".to_string(),
self.block.translate(meta),
format!("{indent}(( {index}++ )) || true"),
"fi".to_string(),
"done".to_string(),
].join("\n")
} else {
[
format!("{index}=0;"),
format!("for {name} in {expr}"),
"do".to_string(),
self.block.translate(meta),
format!("{indent}(( {index}++ )) || true"),
"done".to_string(),
].join("\n")
}
},
None => {
[
format!("for {name} in {expr}"),
"do".to_string(),
self.block.translate(meta),
"done".to_string(),
].join("\n")
if let Some(conditional) = self.iter_expr.conditional(name) {
[
format!("for {name} in {expr}"),
"do".to_string(),
format!("if {conditional}"),
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please note, this will only work with string literals:

#!/usr/bin/env amber

echo "[works]"
loop index, item in glob("*.md") {
    echo "{index}: {item}"
}

echo "[fails]"
let wildcard = "*.md"
loop index, item in glob(wildcard) {
    echo "{index}: {item}"
}

I do not see any way around this (short of calling find, which has its own problems) because Bash glob expansion does not work with quoted globs.

"then".to_string(),
self.block.translate(meta),
"fi".to_string(),
"done".to_string(),
].join("\n")
} else {
[
format!("for {name} in {expr}"),
"do".to_string(),
self.block.translate(meta),
"done".to_string(),
].join("\n")
}
},
}
}
Expand Down
32 changes: 32 additions & 0 deletions src/tests/validity/glob_absolute_missing_file.ab
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import * from "std/text"

// Output
// ===
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These separators are necessary because if the script creates no output (as in this case) the unit test actually behaves as if we had written:

// Output
// Succeded

// ===
// ===

main {
let tmpdir = unsafe $ mktemp -d $
unsafe {
$ touch {tmpdir}/1st\ file\ with\ spaces.txt $
$ touch {tmpdir}/2nd\ file\ with\ spaces.txt $
$ touch {tmpdir}/file1.txt $
$ touch {tmpdir}/file2.txt $
$ touch {tmpdir}/other1.csv $
$ touch {tmpdir}/other2.csv $
}

echo "==="
loop file in glob("{tmpdir}/missing*") {
file = replace_regex(file, tmpdir, "/tmp/tmpdir", true)
echo file
}
echo "==="
loop index, file in glob("{tmpdir}/missing*") {
file = replace_regex(file, tmpdir, "/tmp/tmpdir", true)
echo "{index}: {file}"
}
echo "==="

unsafe $ rm -rf {tmpdir} $
}
40 changes: 40 additions & 0 deletions src/tests/validity/glob_absolute_multiple_globs.ab
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import * from "std/text"

// Output
// ===
// /tmp/tmpdir/file1.txt
// /tmp/tmpdir/file2.txt
// /tmp/tmpdir/other1.csv
// /tmp/tmpdir/other2.csv
// ===
// 0: /tmp/tmpdir/file1.txt
// 1: /tmp/tmpdir/file2.txt
// 2: /tmp/tmpdir/other1.csv
// 3: /tmp/tmpdir/other2.csv
// ===

main {
let tmpdir = unsafe $ mktemp -d $
unsafe {
$ touch {tmpdir}/1st\ file\ with\ spaces.txt $
$ touch {tmpdir}/2nd\ file\ with\ spaces.txt $
$ touch {tmpdir}/file1.txt $
$ touch {tmpdir}/file2.txt $
$ touch {tmpdir}/other1.csv $
$ touch {tmpdir}/other2.csv $
}

echo "==="
loop file in glob("{tmpdir}/missing*", "{tmpdir}/file*", "{tmpdir}/*.csv") {
file = replace_regex(file, tmpdir, "/tmp/tmpdir", true)
echo file
}
echo "==="
loop index, file in glob("{tmpdir}/missing*", "{tmpdir}/file*", "{tmpdir}/*.csv") {
file = replace_regex(file, tmpdir, "/tmp/tmpdir", true)
echo "{index}: {file}"
}
echo "==="

unsafe $ rm -rf {tmpdir} $
}
36 changes: 36 additions & 0 deletions src/tests/validity/glob_absolute_no_spaces.ab
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import * from "std/text"

// Output
// ===
// /tmp/tmpdir/file1.txt
// /tmp/tmpdir/file2.txt
// ===
// 0: /tmp/tmpdir/file1.txt
// 1: /tmp/tmpdir/file2.txt
// ===

main {
let tmpdir = unsafe $ mktemp -d $
unsafe {
$ touch {tmpdir}/1st\ file\ with\ spaces.txt $
$ touch {tmpdir}/2nd\ file\ with\ spaces.txt $
$ touch {tmpdir}/file1.txt $
$ touch {tmpdir}/file2.txt $
$ touch {tmpdir}/other1.csv $
$ touch {tmpdir}/other2.csv $
}

echo "==="
loop file in glob("{tmpdir}/file*") {
file = replace_regex(file, tmpdir, "/tmp/tmpdir", true)
echo file
}
echo "==="
loop index, file in glob("{tmpdir}/file*") {
file = replace_regex(file, tmpdir, "/tmp/tmpdir", true)
echo "{index}: {file}"
}
echo "==="

unsafe $ rm -rf {tmpdir} $
}
36 changes: 36 additions & 0 deletions src/tests/validity/glob_absolute_with_spaces.ab
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import * from "std/text"

// Output
// ===
// /tmp/tmpdir/1st file with spaces.txt
// /tmp/tmpdir/2nd file with spaces.txt
// ===
// 0: /tmp/tmpdir/1st file with spaces.txt
// 1: /tmp/tmpdir/2nd file with spaces.txt
// ===

main {
let tmpdir = unsafe $ mktemp -d $
unsafe {
$ touch {tmpdir}/1st\ file\ with\ spaces.txt $
$ touch {tmpdir}/2nd\ file\ with\ spaces.txt $
$ touch {tmpdir}/file1.txt $
$ touch {tmpdir}/file2.txt $
$ touch {tmpdir}/other1.csv $
$ touch {tmpdir}/other2.csv $
}

echo "==="
loop file in glob("{tmpdir}/*with spaces*") {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The glob builtin does not expect the calling code to escape spaces in its own string literals; it has to do this itself, to prevent injection attacks.

file = replace_regex(file, tmpdir, "/tmp/tmpdir", true)
echo file
}
echo "==="
loop index, file in glob("{tmpdir}/*with spaces*") {
file = replace_regex(file, tmpdir, "/tmp/tmpdir", true)
echo "{index}: {file}"
}
echo "==="

unsafe $ rm -rf {tmpdir} $
}
Loading