Skip to content

Commit

Permalink
Merge pull request #204 from mbland/fileutil-module-#184
Browse files Browse the repository at this point in the history
Add lib/fileutil file and directory tools module
  • Loading branch information
mbland committed Sep 4, 2017
2 parents ea19376 + b8181a7 commit 5761ec8
Show file tree
Hide file tree
Showing 5 changed files with 296 additions and 3 deletions.
95 changes: 95 additions & 0 deletions lib/fileutil
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
#! /usr/bin/env bash
#
# File and directory management
#
# Exports:
# @go.create_dirs
# Creates a set of directories and any missing parents
#
# @go.collect_file_paths
# Collects all the paths to regular files within a directory structure

. "$_GO_USE_MODULES" 'log' 'path'

# Creates a set of directories and any missing parents
#
# If the directory already exists, this will not update its permissions.
#
# Provides a bit more thorough error-checking and feedback than just using
# `mkdir -p` and `chmod -R`. Errors are reported via `@go.log FATAL`, which will
# show a stack trace and crash the program.
#
# Options:
# --mode: Permissions for created directories
#
# Arguments:
# $@: Paths of directories to create
@go.create_dirs() {
local dir
local mode
local __go_missing_parent

if [[ "$1" == '--mode' ]]; then
mode="$2"
shift 2
fi

for dir in "$@"; do
if [[ -z "$dir" ]]; then
@go.log FATAL 'The empty string is not a valid directory name'
elif [[ ! -d "$dir" ]]; then
@go.walk_path_forward '_@go.find_missing_parent_path' "$dir" || :

if [[ -e "$__go_missing_parent" ]]; then
@go.log FATAL "$__go_missing_parent exists and is not a directory"
elif ! mkdir -p "$dir"; then
@go.log FATAL "Could not create $dir in ${__go_missing_parent%/*}"
elif [[ -n "$mode" ]] && ! chmod -R "$mode" "$__go_missing_parent"; then
@go.log FATAL "Could not set permissions for $__go_missing_parent"
fi
fi
done
}

# Collects all the paths to regular files within a directory structure
#
# Globals:
# __go_collected_file_paths: Caller-defined array in which paths are stored
#
# Arguments:
# $@: Paths from which to collect paths for regular files
@go.collect_file_paths() {
__go_collected_file_paths=()
@go.walk_file_system _@go.collect_file_paths_impl "$@"
}

# --------------------------------
# IMPLEMENTATION - HERE BE DRAGONS
#
# None of the functions below this line are part of the public interface.
# --------------------------------

# @go.walk_path_forwared helper to finds the first missing parent directory
#
# Arguments:
# path: Path to examine whether
_@go.find_missing_parent_path() {
__go_missing_parent="$1"

if [[ ! -d "$1" ]]; then
return 1
fi
}

# Helper function for @go.collect_file_paths
#
# Globals:
# __go_collected_file_paths: Caller-defined results array
#
# Arguments:
# path: File system path passed in by @go.walk_file_system
_@go.collect_file_paths_impl() {
if [[ -f "$1" ]]; then
__go_collected_file_paths+=("$1")
fi
}
6 changes: 3 additions & 3 deletions lib/path
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
# result_var_name: Name of the variable into which the result will be stored
# path: Path to canonicalize
@go.canonicalize_path() {
@go.validate_identifier_or_die 'Result variable name' "$1"
@go.validate_identifier_or_die 'Canonicalized path result variable' "$1"
printf -v "$1" '%s' "$2/"

while [[ "${!1}" =~ //+ ]]; do
Expand All @@ -40,14 +40,14 @@
while [[ "${!1}" =~ [^/]+/\.\./ ]]; do
if [[ "${BASH_REMATCH[0]}" != '../../' ]]; then
printf -v "$1" '%s' "${!1/"${BASH_REMATCH[0]}"/}"
elif [[ "${!1}" =~ ^/[./]+ ]]; then
elif [[ "${!1}" =~ ^/[./]+/ ]]; then
printf -v "$1" '%s' "${!1/"${BASH_REMATCH[0]}"//}"
else
break
fi
done

if [[ "${!1}" =~ ^/[./]+ ]]; then
if [[ "${!1}" =~ ^/[./]+/ ]]; then
printf -v "$1" '%s' "${!1/"${BASH_REMATCH[0]}"//}"
fi

Expand Down
53 changes: 53 additions & 0 deletions tests/fileutil/collect-file-paths.bats
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
#! /usr/bin/env bats

load ../environment

setup() {
test_filter
@go.create_test_go_script \
'. "$_GO_USE_MODULES" "fileutil"' \
'declare __go_collected_file_paths' \
'@go.collect_file_paths "$1"' \
'printf "%s\n" "${__go_collected_file_paths[@]}"'
}

teardown() {
@go.remove_test_go_rootdir
}

@test "$SUITE: empty argument list does nothing" {
run "$TEST_GO_SCRIPT"
assert_success ''
}

@test "$SUITE: nonexistent directory path does nothing" {
run "$TEST_GO_SCRIPT" ''
assert_success ''
}

@test "$SUITE: directory with no regular files returns nothing" {
mkdir -p "$TEST_GO_ROOTDIR/foo/bar/baz"
run "$TEST_GO_SCRIPT" "$TEST_GO_ROOTDIR/foo"
assert_success ''
}

@test "$SUITE: directory with a single file" {
mkdir -p "$TEST_GO_ROOTDIR/foo/bar"
printf '%s\n' 'baz' >"$TEST_GO_ROOTDIR/foo/bar/baz"
run "$TEST_GO_SCRIPT" "$TEST_GO_ROOTDIR/foo"
assert_success "$TEST_GO_ROOTDIR/foo/bar/baz"
}

@test "$SUITE: directory with several files" {
mkdir -p "$TEST_GO_ROOTDIR/foo/"{bar,baz,quux}
printf '%s\n' 'xyzzy' >"$TEST_GO_ROOTDIR/foo/bar/xyzzy"
printf '%s\n' 'plugh' >"$TEST_GO_ROOTDIR/foo/baz/plugh"
printf '%s\n' 'frobozz' >"$TEST_GO_ROOTDIR/foo/quux/frobozz"
printf '%s\n' 'frotz' >"$TEST_GO_ROOTDIR/foo/frotz"
run "$TEST_GO_SCRIPT" "$TEST_GO_ROOTDIR/foo"
assert_success \
"$TEST_GO_ROOTDIR/foo/bar/xyzzy" \
"$TEST_GO_ROOTDIR/foo/baz/plugh" \
"$TEST_GO_ROOTDIR/foo/frotz" \
"$TEST_GO_ROOTDIR/foo/quux/frobozz"
}
135 changes: 135 additions & 0 deletions tests/fileutil/create-dirs.bats
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
#! /usr/bin/env bats

load ../environment

setup() {
test_filter
@go.create_test_go_script \
'. "$_GO_USE_MODULES" "fileutil"' \
'@go.create_dirs "$@"'
}

teardown() {
@go.remove_test_go_rootdir
}

assert_dirs_created() {
set "$DISABLE_BATS_SHELL_OPTIONS"
local result='0'
local missing_dirs=()
local dir

for dir in "$@"; do
if [[ ! -d "$TEST_GO_ROOTDIR/$dir" ]]; then
missing_dirs+=("$dir")
fi
done

if [[ "${#missing_dirs[@]}" -ne '0' ]]; then
printf 'The following directories were not created:\n' >&2
printf ' %s\n' "${missing_dirs[@]}" >&2
result='1'
fi
restore_bats_shell_options "$result"
}

@test "$SUITE: empty directory list does nothing" {
run "$TEST_GO_SCRIPT"
assert_success ''
}

@test "$SUITE: empty directory creates FATAL error" {
run "$TEST_GO_SCRIPT" ''
assert_failure
assert_line_matches '0' \
'FATAL.* The empty string is not a valid directory name'
}

@test "$SUITE: existing directory does nothing" {
run "$TEST_GO_SCRIPT" "$TEST_GO_ROOTDIR"
assert_success ''
}

@test "$SUITE: new single directory" {
run "$TEST_GO_SCRIPT" "$TEST_GO_ROOTDIR/foo"
assert_success ''
assert_dirs_created 'foo'
}

@test "$SUITE: permissions set for all new directories" {
skip_if_cannot_trigger_file_permission_failure
local existing_parent="$TEST_GO_ROOTDIR/foo"
mkdir -p "$existing_parent"
chmod 700 "$existing_parent"

run "$TEST_GO_SCRIPT" --mode 723 "$TEST_GO_ROOTDIR/foo/bar/baz"
assert_success ''
assert_dirs_created 'foo'

run ls -ld "$TEST_GO_ROOTDIR/"{foo,foo/bar,foo/bar/baz}
assert_success
assert_lines_match '^drwx------' '^drwx-w--wx' '^drwx-w--wx'
}

@test "$SUITE: multiple calls are idempotent" {
run "$TEST_GO_SCRIPT" "$TEST_GO_ROOTDIR/foo"
assert_success ''
assert_dirs_created 'foo'

run "$TEST_GO_SCRIPT" "$TEST_GO_ROOTDIR/foo"
assert_success ''
}

@test "$SUITE: permissions don't change if directory already exists" {
mkdir -p "$TEST_GO_ROOTDIR/foo"
chmod 700 "$TEST_GO_ROOTDIR/foo"

run "$TEST_GO_SCRIPT" --mode 723 "$TEST_GO_ROOTDIR/foo"
assert_success ''

run ls -ld "$TEST_GO_ROOTDIR/foo"
assert_success
assert_output_matches '^drwx------'
}

@test "$SUITE: multiple directories" {
local dirs=('foo' 'bar/baz' 'bar/quux' 'xyzzy/plugh/frobozz')

run "$TEST_GO_SCRIPT" "${dirs[@]#$TEST_GO_ROOTDIR/}"
assert_success ''
assert_dirs_created "${dirs[@]}"
}

@test "$SUITE: existing non-directory creates a FATAL error" {
mkdir -p "$TEST_GO_ROOTDIR/foo"
printf 'bar\n' >"$TEST_GO_ROOTDIR/foo/bar"

run "$TEST_GO_SCRIPT" "$TEST_GO_ROOTDIR/foo/bar/baz"
assert_failure
assert_line_matches '0' \
"FATAL.* $TEST_GO_ROOTDIR/foo/bar exists and is not a directory"
}

@test "$SUITE: mkdir failure is a FATAL error" {
local existing_parent="$TEST_GO_ROOTDIR/foo"
mkdir -p "$existing_parent"
stub_program_in_path 'mkdir' 'exit 1'

run "$TEST_GO_SCRIPT" "$TEST_GO_ROOTDIR/foo/bar/baz"
restore_program_in_path 'mkdir'
assert_failure
assert_line_matches '0' \
"FATAL.* Could not create $TEST_GO_ROOTDIR/foo/bar/baz in $existing_parent"
}

@test "$SUITE: chmod failure is a FATAL error" {
local existing_parent="$TEST_GO_ROOTDIR/foo"
mkdir -p "$existing_parent"
stub_program_in_path 'chmod' 'exit 1'

run "$TEST_GO_SCRIPT" --mode 700 "$TEST_GO_ROOTDIR/foo/bar/baz"
restore_program_in_path 'chmod'
assert_failure
assert_line_matches '0' \
"FATAL.* Could not set permissions for $existing_parent/bar"
}
10 changes: 10 additions & 0 deletions tests/path-module/canonicalize-path.bats
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,16 @@ run_canonicalize_path() {
assert_success '/'
}

@test "$SUITE: root path relative self" {
run_canonicalize_path '/.'
assert_success '/'
}

@test "$SUITE: root path dotfile" {
run_canonicalize_path '/.bashrc'
assert_success '/.bashrc'
}

@test "$SUITE: leaves relative current dir path unchanged" {
run_canonicalize_path '.'
assert_success '.'
Expand Down

0 comments on commit 5761ec8

Please sign in to comment.