Skip to content

Commit

Permalink
Implement standard library glob function (#511)
Browse files Browse the repository at this point in the history
* Reinstate command to remove /tmp directory.

* Add glob functions to standard library.

* Whitespace changes.

* Reinstate removed blank lines.
  • Loading branch information
hdwalters authored Oct 22, 2024
1 parent 2a686c7 commit 254e102
Show file tree
Hide file tree
Showing 13 changed files with 477 additions and 1 deletion.
32 changes: 32 additions & 0 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 @@ -76,3 +78,33 @@ pub fun change_owner(user: Text, path: Text): Bool {

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])?
}
2 changes: 1 addition & 1 deletion src/tests/stdlib/create_symbolic_link.ab
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,5 @@ main {
} else {
echo "failed"
}
trust $rm {tmpdir}$
trust $rm -fr {tmpdir}$
}
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} $
}
47 changes: 47 additions & 0 deletions src/tests/stdlib/glob_absolute_multiple_globs.ab
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
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} $
}
44 changes: 44 additions & 0 deletions src/tests/stdlib/glob_absolute_wild_char.ab
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
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} $
}
46 changes: 46 additions & 0 deletions src/tests/stdlib/glob_absolute_wild_star.ab
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
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} $
}
44 changes: 44 additions & 0 deletions src/tests/stdlib/glob_absolute_with_spaces.ab
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
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} $
}
48 changes: 48 additions & 0 deletions src/tests/stdlib/glob_relative_multiple_globs.ab
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
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} $
}
Loading

0 comments on commit 254e102

Please sign in to comment.