Skip to content

Commit

Permalink
Add glob functions to standard library.
Browse files Browse the repository at this point in the history
  • Loading branch information
hdwalters committed Oct 19, 2024
1 parent ce16ee8 commit d9569af
Show file tree
Hide file tree
Showing 12 changed files with 442 additions and 3 deletions.
35 changes: 32 additions & 3 deletions src/std/fs.ab
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { join, len, replace_regex, split } from "std/text"

/// Checks if a directory exists.
pub fun dir_exist(path) {
$[ -d "{path}" ]$ failed {
Expand Down Expand Up @@ -40,7 +42,6 @@ pub fun create_symbolic_link(origin: Text, destination: Text): Bool {
trust $ln -s "{origin}" "{destination}"$
return true
}

echo "The file {origin} doesn't exist!"
return false
}
Expand All @@ -60,7 +61,6 @@ pub fun make_executable(path: Text): Bool {
trust $chmod +x "{path}"$
return true
}

echo "The file {path} doesn't exist!"
return false
}
Expand All @@ -73,6 +73,35 @@ pub fun change_owner(user: Text, path: Text): Bool {
trust $chown -R "{user}" "{path}"$
return true
}

return false
}

/// Escapes all characters in the passed-in glob except "*", "?" and "/",
/// to prevent injection attacks.
fun escape_non_glob_chars(path: Text): Text {
return replace_regex(path, "\([^*?/]\)", "\\\\\1")
}

/// Finds all files or directories matching multiple file globs. When
/// we have union types, this functionality can be merged into the main
/// `glob` function.
pub fun glob_multiple(paths: [Text]): [Text]? {
let combined = ""
if len(paths) == 1 {
combined = escape_non_glob_chars(paths[0])
} else {
let items = [Text]
loop item in paths {
item = escape_non_glob_chars(item)
items += [item]
}
combined = join(items, " ")
}
let files = $eval "for file in {combined}; do [ -e \\\"\\\$file\\\" ] && echo \\\"\\\$file\\\"; done"$?
return split(files, "\n")
}

/// Finds all files or directories matching a file glob.
pub fun glob(path: Text): [Text]? {
return glob_multiple([path])?
}
26 changes: 26 additions & 0 deletions src/tests/stdlib/glob_absolute_missing_file.ab
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import * from "std/fs"

// Output
// FAILED

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

let files = glob("{tmpdir}/missing*") failed {
echo "FAILED"
}
loop file in files {
echo "[{file}]"
}

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

fun compare(actual: [Text], expected: [Text]): Bool {
if len(actual) != len(expected) {
return false
}
loop file in expected {
if not includes(actual, file) {
return false
}
}
return true
}

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

let expected = ["{tmpdir}/file.txt", "{tmpdir}/file1.txt", "{tmpdir}/file2.txt", "{tmpdir}/file99.txt", "{tmpdir}/other.csv"]
let actual = glob_multiple(["{tmpdir}/missing*", "{tmpdir}/file*.txt", "{tmpdir}/other*.csv"]) failed {
echo "FAILED"
}
if compare(actual, expected) {
echo "Succeded"
} else {
echo "Expected: {expected}"
echo "Actual: {actual}"
}

trust $ rm -rf {tmpdir} $
}
41 changes: 41 additions & 0 deletions src/tests/stdlib/glob_absolute_wild_char.ab
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import * from "std/array"
import * from "std/fs"
import * from "std/text"

fun compare(actual: [Text], expected: [Text]): Bool {
if len(actual) != len(expected) {
return false
}
loop file in expected {
if not includes(actual, file) {
return false
}
}
return true
}

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

let expected = ["{tmpdir}/file1.txt", "{tmpdir}/file2.txt"]
let actual = glob("{tmpdir}/file?.txt") failed {
echo "FAILED"
}
if compare(actual, expected) {
echo "Succeded"
} else {
echo "Expected: {expected}"
echo "Actual: {actual}"
}

trust $ rm -rf {tmpdir} $
}
41 changes: 41 additions & 0 deletions src/tests/stdlib/glob_absolute_wild_star.ab
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import * from "std/array"
import * from "std/fs"
import * from "std/text"

fun compare(actual: [Text], expected: [Text]): Bool {
if len(actual) != len(expected) {
return false
}
loop file in expected {
if not includes(actual, file) {
return false
}
}
return true
}

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

let expected = ["{tmpdir}/file.txt", "{tmpdir}/file1.txt", "{tmpdir}/file2.txt", "{tmpdir}/file99.txt"]
let actual = glob("{tmpdir}/file*.txt") failed {
echo "FAILED"
}
if compare(actual, expected) {
echo "Succeded"
} else {
echo "Expected: {expected}"
echo "Actual: {actual}"
}

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

fun compare(actual: [Text], expected: [Text]): Bool {
if len(actual) != len(expected) {
return false
}
loop file in expected {
if not includes(actual, file) {
return false
}
}
return true
}

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

let expected = ["{tmpdir}/1st file with spaces.txt", "{tmpdir}/2nd file with spaces.txt"]
let actual = glob("{tmpdir}/*with spaces*") failed {
echo "FAILED"
}
if compare(actual, expected) {
echo "Succeded"
} else {
echo "Expected: {expected}"
echo "Actual: {actual}"
}

trust $ rm -rf {tmpdir} $
}
25 changes: 25 additions & 0 deletions src/tests/stdlib/glob_injection_attack.ab
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import * from "std/fs"

// Output
// [xxx; do echo HACKED; done; for file in]

main {
let tmpdir = trust $ mktemp -d $
trust {
$ touch "{tmpdir}/xxx; do echo HACKED; done; for file in" $
}
cd tmpdir

// The glob function escapes all characters in the passed-in glob
// apart from "*", "?" and "/", to prevent injection attacks. If we
// didn't do this, the following code would output "[HACKED]" instead
// of the filename.
let files = glob("xxx; do echo HACKED; done; for file in") failed {
echo "FAILED"
}
loop file in files {
echo "[{file}]"
}

trust $ rm -rf {tmpdir} $
}
27 changes: 27 additions & 0 deletions src/tests/stdlib/glob_relative_missing_file.ab
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import * from "std/fs"

// Output
// FAILED

main {
let tmpdir = trust $ mktemp -d $
trust {
$ touch {tmpdir}/1st\ file\ with\ spaces.txt $
$ touch {tmpdir}/2nd\ file\ with\ spaces.txt $
$ touch {tmpdir}/file.txt $
$ touch {tmpdir}/file1.txt $
$ touch {tmpdir}/file2.txt $
$ touch {tmpdir}/file99.txt $
$ touch {tmpdir}/other.csv $
}
cd tmpdir

let files = glob("missing*") failed {
echo "FAILED"
}
loop file in files {
echo "[{file}]"
}

trust $ rm -rf {tmpdir} $
}
42 changes: 42 additions & 0 deletions src/tests/stdlib/glob_relative_multiple_globs.ab
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import * from "std/array"
import * from "std/fs"
import * from "std/text"

fun compare(actual: [Text], expected: [Text]): Bool {
if len(actual) != len(expected) {
return false
}
loop file in expected {
if not includes(actual, file) {
return false
}
}
return true
}

main {
let tmpdir = trust $ mktemp -d $
trust {
$ touch {tmpdir}/1st\ file\ with\ spaces.txt $
$ touch {tmpdir}/2nd\ file\ with\ spaces.txt $
$ touch {tmpdir}/file.txt $
$ touch {tmpdir}/file1.txt $
$ touch {tmpdir}/file2.txt $
$ touch {tmpdir}/file99.txt $
$ touch {tmpdir}/other.csv $
}
cd tmpdir

let expected = ["file.txt", "file1.txt", "file2.txt", "file99.txt", "other.csv"]
let actual = glob_multiple(["missing*", "file*.txt", "other*.csv"]) failed {
echo "FAILED"
}
if compare(actual, expected) {
echo "Succeded"
} else {
echo "Expected: {expected}"
echo "Actual: {actual}"
}

trust $ rm -rf {tmpdir} $
}
42 changes: 42 additions & 0 deletions src/tests/stdlib/glob_relative_wild_char.ab
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import * from "std/array"
import * from "std/fs"
import * from "std/text"

fun compare(actual: [Text], expected: [Text]): Bool {
if len(actual) != len(expected) {
return false
}
loop file in expected {
if not includes(actual, file) {
return false
}
}
return true
}

main {
let tmpdir = trust $ mktemp -d $
trust {
$ touch {tmpdir}/1st\ file\ with\ spaces.txt $
$ touch {tmpdir}/2nd\ file\ with\ spaces.txt $
$ touch {tmpdir}/file.txt $
$ touch {tmpdir}/file1.txt $
$ touch {tmpdir}/file2.txt $
$ touch {tmpdir}/file99.txt $
$ touch {tmpdir}/other.csv $
}
cd tmpdir

let expected = ["file1.txt", "file2.txt"]
let actual = glob("file?.txt") failed {
echo "FAILED"
}
if compare(actual, expected) {
echo "Succeded"
} else {
echo "Expected: {expected}"
echo "Actual: {actual}"
}

trust $ rm -rf {tmpdir} $
}
Loading

0 comments on commit d9569af

Please sign in to comment.