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 standard library glob function #511

Merged
merged 4 commits into from
Oct 22, 2024
Merged
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
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 {

hdwalters marked this conversation as resolved.
Show resolved Hide resolved
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, " ")
}
Ph0enixKM marked this conversation as resolved.
Show resolved Hide resolved
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}$
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I believe this is also part of a different pending PR. Shouldn't cause merge conflicts, but this is implemented in a commit of its own, so I can drop it and rebase if it causes problems.

}
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