aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorGravatar Laszlo Csomor <laszlocsomor@google.com>2016-09-15 12:30:46 +0000
committerGravatar Dmitry Lomov <dslomov@google.com>2016-09-15 13:08:59 +0000
commitcf773d29e7001293638f3411c33622a4598556b2 (patch)
tree7b08d0317166f8557296f548e35ae5642e3cca0e
parentb537962b3d3ed76e971f566ce597737de7108d39 (diff)
Implement `realpath(1)` using basic Bash utils.
Neither `realpath` nor `readlink -e` are available on Mac OS X, so I implemented one using basic Bash utilities: `readlink`, `basename`, and `dirname`. I also implemented a `normalize_path` method that can normalize relative or absolute path strings (remove and resolve "." and ".." references). It uses simple string processing and doesn't touch the file system. This will help fixing https://github.com/bazelbuild/bazel/issues/1776 and also opensourcing some shell tests that rely on `realpath`. -- MOS_MIGRATED_REVID=133249912
-rw-r--r--src/test/shell/BUILD15
-rwxr-xr-xsrc/test/shell/shell_utils.sh106
-rwxr-xr-xsrc/test/shell/shell_utils_test.sh204
3 files changed, 325 insertions, 0 deletions
diff --git a/src/test/shell/BUILD b/src/test/shell/BUILD
index 95c387b055..6ab4cb8e33 100644
--- a/src/test/shell/BUILD
+++ b/src/test/shell/BUILD
@@ -24,3 +24,18 @@ sh_test(
srcs = ["unittest_test.sh"],
data = [":bashunit"],
)
+
+sh_library(
+ name = "shell_utils",
+ srcs = ["shell_utils.sh"],
+)
+
+sh_test(
+ name = "shell_utils_test",
+ srcs = ["shell_utils_test.sh"],
+ data = [
+ "testenv.sh",
+ ":bashunit",
+ ":shell_utils",
+ ],
+)
diff --git a/src/test/shell/shell_utils.sh b/src/test/shell/shell_utils.sh
new file mode 100755
index 0000000000..2c9c4bdb73
--- /dev/null
+++ b/src/test/shell/shell_utils.sh
@@ -0,0 +1,106 @@
+#!/bin/bash
+#
+# Copyright 2016 The Bazel Authors. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+set -euo pipefail
+
+# Print the resolved path of a symbolic link (or the path itself if not a link).
+#
+# The result may be relative to the current working directory and may contain
+# "." and ".." references. Use `normalize_path` to remove those.
+#
+# Fail and print nothing if the input path doesn't exist, i.e. it's a
+# non-existent file, dangling symlink, or circular symlink.
+function resolve_links() {
+ local name="$1"
+
+ if [ -e "$name" ]; then
+ # resolve all links, keep path absolute
+ while [ -L "$name" ]; do
+ local target=$(readlink "$name")
+ if [ "$(echo "$target" | head -c1)" = "/" ]; then
+ name="$target"
+ else
+ name="$(dirname "$name")/$target"
+ fi
+ done
+ echo "$name"
+ else
+ false # fail the function
+ fi
+}
+
+# Normalize a path string by removing "." and ".." references from it.
+#
+# Print the result to stdout.
+#
+# The path doesn't have to point to an existing file.
+function normalize_path() {
+ local name="$1"
+
+ local path=""
+ local uplevels=0
+ while [ "$name" != "/" -a "$name" != "." ]; do
+ local segment="$(basename "$name")"
+ name="$(dirname "$name")"
+
+ [ "$segment" = "." ] && continue
+
+ if [ "$segment" = ".." ]; then
+ uplevels="$((${uplevels}+1))"
+ else
+ if [ "$uplevels" -gt 0 ]; then
+ uplevels="$((${uplevels}-1))"
+ else
+ path="${segment}/${path}"
+ fi
+ fi
+ done
+
+ if [ "$name" = "." ]; then
+ while [ "$uplevels" -gt 0 ]; do
+ path="../$path"
+ uplevels="$((${uplevels}-1))"
+ done
+ fi
+
+ path="${path%/}"
+
+ if [ "$name" = "/" ]; then
+ echo "/$path"
+ else
+ [ -n "$path" ] && echo "$path" || echo "."
+ fi
+}
+
+# Custom implementation of `realpath(1)`.
+#
+# Resolves a symlink to the absolute path it points to. Fails if the link or
+# its target does not exist.
+#
+# Fails and prints nothing if the input path doesn't exist, i.e. it's a
+# non-existent file, dangling symlink, or circular symlink.
+#
+# We use this method instead of `realpath(1)` because the latter is not
+# available on Mac OS X by default.
+function get_real_path() {
+ local name="$1"
+ name="$(resolve_links "$name" || echo)"
+ if [ -n "$name" ]; then
+ normalize_path "$(pwd)/$name"
+ else
+ false # fail the function
+ fi
+}
diff --git a/src/test/shell/shell_utils_test.sh b/src/test/shell/shell_utils_test.sh
new file mode 100755
index 0000000000..cc23345431
--- /dev/null
+++ b/src/test/shell/shell_utils_test.sh
@@ -0,0 +1,204 @@
+#!/bin/bash
+#
+# Copyright 2016 The Bazel Authors. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+# This test exercises Bash utility implementations.
+
+source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/unittest.bash" \
+ || { echo "Could not source unittest.sh" >&2; exit 1; }
+
+source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/shell_utils.sh" \
+ || { echo "shell_utils.sh not found!" >&2; exit 1; }
+
+set -eu
+
+cd "$TEST_TMPDIR"
+
+function assert_fails_to() {
+ local -r method="$1"
+ local -r path="$2"
+ "$method" "$path" && fail "Symlink resolution for '$path' should have failed"
+ true # reset the exit status otherwise the test would be considered failed
+}
+
+function test_resolve_bad_links() {
+ local -r dir="${FUNCNAME[0]}"
+
+ mkdir -p "$dir" || fail "mkdir -p $dir"
+
+ # absolute, non-existent path
+ assert_fails_to resolve_links "$(pwd)/${dir}/non-existent"
+
+ # relative, non-existent path
+ assert_fails_to resolve_links "${dir}/non-existent"
+
+ # symlink with absolute non-existent target
+ ln -s "/non-existent" "${dir}/bad-absolute.sym"
+ assert_fails_to resolve_links "$(pwd)/${dir}/bad-absolute.sym"
+ assert_fails_to resolve_links "${dir}/bad-absolute.sym"
+
+ # symlink with relative non-existent target
+ ln -s "non-existent" "${dir}/bad-relative.sym"
+ assert_fails_to resolve_links "$(pwd)/${dir}/bad-relative.sym"
+ assert_fails_to resolve_links "${dir}/bad-relative.sym"
+
+ # circular symlink
+ ln -s "circular.sym" "${dir}/circular.sym"
+ assert_fails_to resolve_links "$(pwd)/${dir}/circular.sym"
+ assert_fails_to resolve_links "${dir}/circular.sym"
+}
+
+function test_resolve_non_links() {
+ local -r dir="${FUNCNAME[0]}"
+
+ mkdir -p "$dir" || fail "mkdir -p $dir"
+ echo hello > "${dir}/hello.txt"
+
+ # absolute path to directory
+ assert_equals "$(pwd)" "$(resolve_links "$(pwd)")"
+
+ # relative path to directory
+ assert_equals "${dir}" "$(resolve_links "$dir")"
+
+ # relative path to file
+ assert_equals "${dir}/hello.txt" "$(resolve_links "${dir}/hello.txt")"
+
+ # absolute path to file
+ assert_equals \
+ "$(pwd)/${dir}/hello.txt" "$(resolve_links "$(pwd)/${dir}/hello.txt")"
+}
+
+function test_resolve_symlinks() {
+ local -r dir="${FUNCNAME[0]}"
+
+ mkdir -p "${dir}/a/b" || fail "mkdir -p ${dir}/a/b"
+ echo hello > "${dir}/hello.txt"
+
+ ln -s "." "${dir}/self"
+ ln -s "../hello.txt" "${dir}/a/sym"
+ ln -s "../sym" "${dir}/a/b/sym"
+ ln -s ".././sym" "${dir}/a/b/sym-not-normalized"
+
+ assert_equals "${dir}/." "$(resolve_links "${dir}/self")"
+ assert_equals "${dir}/a/../hello.txt" "$(resolve_links "${dir}/a/sym")"
+ assert_equals "${dir}/a/b/../../hello.txt" "$(resolve_links "${dir}/a/b/sym")"
+ assert_equals \
+ "${dir}/a/b/.././../hello.txt" \
+ "$(resolve_links "${dir}/a/b/sym-not-normalized")"
+
+ cd "$dir"
+ assert_equals "./." "$(resolve_links "self")"
+ assert_equals "./." "$(resolve_links "./self")"
+ assert_equals "a/../hello.txt" "$(resolve_links "a/sym")"
+ assert_equals "a/b/../../hello.txt" "$(resolve_links "a/b/sym")"
+ assert_equals \
+ "a/b/.././../hello.txt" \
+ "$(resolve_links "a/b/sym-not-normalized")"
+
+ cd a
+ assert_equals "../." "$(resolve_links "../self")"
+ assert_equals "./../hello.txt" "$(resolve_links "sym")"
+ assert_equals "./../hello.txt" "$(resolve_links "./sym")"
+ assert_equals "b/../../hello.txt" "$(resolve_links "b/sym")"
+ assert_equals \
+ "b/.././../hello.txt" \
+ "$(resolve_links "b/sym-not-normalized")"
+
+ cd b
+ assert_equals "../../." "$(resolve_links "../../self")"
+ assert_equals "../../hello.txt" "$(resolve_links "../sym")"
+ assert_equals "./../../hello.txt" "$(resolve_links "sym")"
+ assert_equals "./../../hello.txt" "$(resolve_links "./sym")"
+ assert_equals \
+ "./.././../hello.txt" \
+ "$(resolve_links "sym-not-normalized")"
+}
+
+function test_normalize_path() {
+ assert_equals "." "$(normalize_path "")"
+ assert_equals "." "$(normalize_path ".")"
+ assert_equals "." "$(normalize_path "./.")"
+ assert_equals ".." "$(normalize_path "..")"
+ assert_equals ".." "$(normalize_path "./..")"
+ assert_equals ".." "$(normalize_path "../.")"
+ assert_equals "../.." "$(normalize_path "../././..")"
+
+ assert_equals "blah" "$(normalize_path "blah")"
+ assert_equals "blah" "$(normalize_path "blah/.")"
+ assert_equals "blah" "$(normalize_path "blah/./hello/..")"
+ assert_equals "blah" "$(normalize_path "blah/./hello/../")"
+ assert_equals \
+ "blah/hello" "$(normalize_path "blah/./hello/../a/b/.././.././hello")"
+ assert_equals "." "$(normalize_path "blah/./hello/../a/b/.././.././..")"
+ assert_equals ".." "$(normalize_path "blah/.././..")"
+ assert_equals "../.." "$(normalize_path "blah/.././../..")"
+
+ assert_equals "/" "$(normalize_path "/")"
+ assert_equals "/" "$(normalize_path "/.")"
+ assert_equals "/" "$(normalize_path "/./.")"
+ assert_equals "/blah" "$(normalize_path "/blah")"
+ assert_equals "/blah" "$(normalize_path "/blah/.")"
+ assert_equals "/blah" "$(normalize_path "/blah/./hello/..")"
+ assert_equals "/blah" "$(normalize_path "/blah/./hello/../")"
+ assert_equals "/blah" "$(normalize_path "/blah/./hello/../a/b/.././../.")"
+}
+
+function test_get_realpath() {
+ local -r dir="${FUNCNAME[0]}"
+
+ mkdir -p "${dir}/a/b" || fail "mkdir -p ${dir}/a/b"
+ echo hello > "${dir}/hello.txt"
+
+ ln -s "." "${dir}/self"
+ ln -s "../hello.txt" "${dir}/a/sym"
+ ln -s "../sym" "${dir}/a/b/sym"
+ ln -s ".././sym" "${dir}/a/b/sym-not-normalized"
+
+ assert_equals "$(pwd)/${dir}" "$(get_real_path "${dir}/self")"
+ assert_equals "$(pwd)/${dir}/hello.txt" "$(get_real_path "${dir}/a/sym")"
+ assert_equals "$(pwd)/${dir}/hello.txt" "$(get_real_path "${dir}/a/b/sym")"
+ assert_equals \
+ "$(pwd)/${dir}/hello.txt" \
+ "$(get_real_path "${dir}/a/b/sym-not-normalized")"
+
+ cd "$dir"
+ local -r abs_dir=$(pwd)
+ assert_equals "${abs_dir}" "$(get_real_path "self")"
+ assert_equals "${abs_dir}" "$(get_real_path "./self")"
+ assert_equals "${abs_dir}/hello.txt" "$(get_real_path "a/sym")"
+ assert_equals "${abs_dir}/hello.txt" "$(get_real_path "a/b/sym")"
+ assert_equals \
+ "${abs_dir}/hello.txt" "$(get_real_path "a/b/sym-not-normalized")"
+
+ cd a
+ assert_equals "${abs_dir}" "$(get_real_path "../self")"
+ assert_equals "${abs_dir}/hello.txt" "$(get_real_path "sym")"
+ assert_equals "${abs_dir}/hello.txt" "$(get_real_path "./sym")"
+ assert_equals "${abs_dir}/hello.txt" "$(get_real_path "b/sym")"
+ assert_equals "${abs_dir}/hello.txt" "$(get_real_path "b/sym-not-normalized")"
+
+ cd b
+ assert_equals "${abs_dir}" "$(get_real_path "../../self")"
+ assert_equals "${abs_dir}/hello.txt" "$(get_real_path "../sym")"
+ assert_equals "${abs_dir}/hello.txt" "$(get_real_path "sym")"
+ assert_equals "${abs_dir}/hello.txt" "$(get_real_path "./sym")"
+ assert_equals "${abs_dir}/hello.txt" "$(get_real_path "sym-not-normalized")"
+
+ assert_fails_to get_real_path "non-existent"
+ ln -s self self
+ assert_fails_to get_real_path "self"
+}
+
+run_suite "Tests for Bash utilities"