# Three Dots: Feature Request/Philosophical Bug Report
## 1. Proposal
**Proposal:** Let `.../` (three dots) replace any number of `../`
(parent directory) symbols, provided that the file name that follows
the dots exists somewhere above in the directory tree.
For example, we can use `.../gradlew build` instead
of both `../../../gradlew build` and `./gradlew build`, from any
directory within the project tree.
So, `.../foo.bar` means "the file `foo.bar` in the current directory
or some ancestor directory."
In other words, `.../foo.bar` means either `./foo.bar`,
or `../foo.bar`, or `../../foo.bar`, or `../../../foo.bar`, etc., the
one that is found first. (This also means that the first
file-or-directory name that follows three dots must exist somewhere).
**What is the added value?** Scripts that reference items in parent
directories become _depth-independent_.
**In what cases is it useful?** Software projects often have large and
complex source trees, and sometimes people use scripts to perform
custom tasks in these source trees. With depth independence, your
scripts do not break when the project is refactored, and directory
levels are added or removed. With depth independence, you can reuse
the same script in subtrees that have similar but slightly different
structures. Without depth independence, you may end up with multiple
scripts that differ only in the number of parent directory symbols. In
addition, three dots are more readable; human beings do not like to
count dots, and "file `foo.bar`, somewhere above" is what a person
thinks when seeing `../../../../foo.bar`.
**But what if my project has a `foo.bar` in each directory?** You
should reference such files using the containing directory
name(s), like `.../baaz/foo.bar` or `.../quux/foo.bar`. Anyway,
using `../../../foo.bar` is a recipe for bugs; you probably should
have used `../../../../baaz/foo.bar` instead.
Three dots expansion MUST be performed after variable expansion but
before passing paths to file I/O functions.
Our language and our thinking are related, and an important aspect of
this proposal is that we extend human thinking with the idea that such
search in ancestor directories is possible and may be used for
scripting.
That's the proposal. The rest of this text discusses technical details
and corner cases. (But note that the Appendices 1 and 2 show practical
use of the proposed idea by existing means.)
### == TL;DR ==
### 1.1 Generalized parent directory notation
We allow things like `foo/..` where `foo` is a file rather than a
directory. `$FILEPATH/..` means `$(dirname $FILEPATH)`, the directory
where the file is (or would be) located.
### 1.2 Why `.../` starts searching from `./` ?
To let things like `.../gradlew build` work from anywhere in the
project tree, `.../` starts searching from the current directory `./`
The idea is that if some file is located in the project's root
directory, scripts run from the project's root must find it.
### 1.3 Variations: how to start searching from the parent directory
Since `.../` (zero or more levels up) may mean `./` and often this is
not what is desired, here are two more notations:
- `..../` is equivalent to `../.../` (at least one level up)
- `...../` is equivalent to `../../.../` (at least two levels up)
It is more of less clear why `..../` is proposed (because sometimes we
don't need searching in the current directory, but this is what `.../`
does, so we would use `../.../`).
The use case for `...../` is finding the second file `foo.txt` in the
directory tree: `.../foo.txt/...../foo.txt` is equivalent
to `.../foo.txt/../../.../foo.txt`, and if we remove one or two `../`
from this construct, we will get the first file `.../foo.txt`.
(Indeed, let's consider `.../foo.txt/../.../foo.txt`,
here `.../foo.txt/../` is the directory where `foo.txt` is
located, `...` will start searching from the directory where `foo.txt`
is located, and of course will find `foo.txt` in the directory
where `foo.txt` is located, it sounds stupid but the result will be
stupid too, so `.../foo.txt/../.../foo.txt` will be the same
as `.../foo.txt`. To find the 2nd `foo.txt`, we need two parent
directory symbols, one for the file, the other for its
directory: `.../foo.txt/../../.../foo.txt`.)
Note that the use case for both `..../` and `...../` is searching from
the parent directory, but in the first case we mean "the parent
directory of the current directory," while in the second case we
mean "the parent directory of the found file."
Both `..../` and `...../` are just a syntactic sugar; the really
important thing, the one that introduces a feature that was not
previously available, is `.../` (three dots).
## == TL;DR ==
## 2. Specification (what exactly is done when, in terms of substring
replacement)
For the purpose of this text, both terms _"file"_ and
_"file-or-directory"_ mean any filesystem entity, including ordinary
files, directories, block and character devices, mount points,
symbolic links, named pipes, sockets, and whatever else you can find
in a file system.
When an _ordinary file_ is meant, it is explicitly disambiguated.
### 2.1 `.../foo.bar`
Three dots MUST be followed by a file-or-directory name.
The notation `.../foo.bar` means: in the sequence `./foo.bar`
, `../foo.bar` , `../../foo.bar` , `../../../foo.bar` , etc. find the
first file-or-directory that exists; interpret `.../foo.bar` as a
reference to that file-or-directory.
It is important that the file name after three dots must exist.
The notation `../.../foo.bar` makes sure that the resulting path
contains at least one `../`, that is, the file search starts from the
parent directory.
Extension: `..../` is equivalent to `../.../` (the file search starts
from the parent directory), and `...../` is equivalent to `../../.../`
The notation `a/b/c/d/e/f/.../foo.bar` is valid, it means that the
search in the ancestor tree must start from the
directory `a/b/c/d/e/f/`. (That is, `cd` to `a/b/c/d/e/f/` and then
search the ancestor tree.)
The notation `.../../` is currently invalid (reserved for future
versions). (It is possible to attribute a meaning to it: `.../../foo`
would mean "such directory that `../foo` exists", and `..././foo`
would mean `.../foo/..`, but there would be some inconsistency, and
there's no need to do that in the 1st version.)
The notation `..././` is currently invalid (reserved for future
versions).
The notation `/.../` denotes the root directory `/` because the root
directory `/` ~~has no parent~~ is its own parent. For example, if the
current directory is `/`, then `$(pwd)/.../foo.bar` is the same
as `/foo.bar`.
Note that a path that ends with `.../` or `...` is invalid, so e.g.
when we say that `/.../` means the root directory `/`, we imply that
it's followed by an existing file-or-directory name.
When the user-provided path is invalid, the implementation MAY try to
interpret `...`, `....`, `.....` as file-or-directory names.
Three dots expansion MUST be performed after variable expansion (so
that `x='...'; cat $x/foo.bar` would work), but before passing paths
to file I/O functions.
### 2.2 `.../foo.txt/..`
Three dots MAY be followed by a file-or-directory-name, slash, two
dots. The notation `.../foo.bar/..` denotes the directory that
contains the file-or-directory `foo.bar`. In other words: _"search the
parent directories and find the first existing file or
directory `foo.bar`; replace `.../` by zero or more `../` so that this
sequence of parent directory symbols refers to the directory
containing that found file-or-directory `foo.bar`; remove `foo.bar/..`
from the resulting path; interpret `.../foo.bar/..` as that resulting
path."_
Note that we allow any file, even an ordinary file, to be mentioned
between `.../` and `/..` , that is, in `.../foo/..`, `foo` need not be
a directory (note that `ordinary_file/..` is not allowed in modern
bash).
### 2.3 `mkdir .../foo/bar`
`.../foo/bar` means "file-or-directory `bar` in the
directory `.../foo/`"
If `.../` is followed by more than one file name, only the first file
name is searched for. For example, if we must reference an existing
file, and the path is `.../foo/bar`, and `bar` does not exist in the
nearest directory `foo`, a file-not-found error will be reported (even
if there is another ancestor directory that contains `foo` that
contains `bar`). On the other hand, in the general case the referenced
file-or-directory is not required to exist, e.g. `mkdir .../foo/bar`
will create the directory `bar` in the nearest ancestor directory
named `foo`.
### 2.4 `.../foo/...../foo`
The three dots symbol `.../` may be used multiple times in one path
expression, e.g. `.../build.gradle/../../.../build.gradle` means the
2nd found `build.gradle`.
Extension: `...../` (five dots) is equivalent to `../../.../` (two
level-ups and three dots), e.g. `.../build.gradle/...../build.gradle`
is equivalent to `.../build.gradle/../../.../build.gradle`
## == TL;DR;;TL;DR ==
## 3. Test cases
Test cases are provided here as yet another way to disambiguate what
may be ambiguous.
### 3.1 Prerequisites: asserteq
```
$ cat `which asserteq`
expected="$1"
actual="$2"
message="$3"
if [ "$expected" != "$actual" ]; then
echo "Assertion failed: $message (Expected: '$expected', Actual:
'$actual')" >&2
exit 1
fi
```
### 3.2 Prerequisites: test directory
```
sudo mkdir /test
sudo chmod a+rwx /test
cd /test
asserteq /test `pwd`
```
### 3.3 Test cases
#### Requirement A: must work for directory names
Desired: use `.../src/` to denote the nearest directory `src` "above"
in the ancestor directories in the directory tree
What we have now: we use `../../../../../../../src/` and we must count
the dots
```
cd /test && rm -rf *
mkdir -p src/a/b/c/d
cd src/a/b/c/d
cd .../src
asserteq /test/src `pwd` "A1 must go to ancestor directory"
```
```
cd /test && rm -rf *
mkdir src
mkdir -p a/b/c/d
cd a/b/c/d
cd .../src
asserteq /test/src `pwd` "A2 must go to ancestor's sibling directory"
```
```
cd /test && rm -rf *
mkdir -p src/a/b/c/d/tao
mkdir tao
cd src/a/b/c/d
cd .../tao
asserteq /test/src/a/b/c/d/tao `pwd` "A3 must go to child directory"
cd .../tao
asserteq /test/src/a/b/c/d/tao `pwd` "A4 must remain in the destination
directory"
cd ../.../tao
asserteq /test/src/a/b/c/d/tao `pwd` "A5 gotcha: ../... searches the
parent directory and finds the current directory"
cd ../../.../tao
asserteq /test/tao `pwd` "A6 requires ../../.../ to go to ancestor's
sibling"
cd .../src/a/b/c/d/tao
asserteq /test/src/a/b/c/d/tao `pwd` "A7 must allow multiple file names
after slash"
```
#### Requirement B: must work for file names
Desired: use `.../build.gradle` to denote the nearest
file `build.gradle` somewhere above in the tree
What we have now: we use `../../../../../../../build.gradle` and we
must count the dots
```
cd /test && rm -rf *
echo "bar" >foo
asserteq "bar" "`cat foo`" "B0.1 test framework sanity check"
mkdir -p src/a/b/c/d
asserteq "bar" "`cat .../foo`" "B1 must find file in the current
directory"
cd src
asserteq "bar" "`cat .../foo`" "B2 must find file in the parent
directory"
cd a/
asserteq "bar" "`cat .../foo`" "B3 must find file in the parent's
parent directory"
cd b/c/d
asserteq "bar" "`cat .../foo`" "B4 must find file in any ancestor
directory"
asserteq /test/src/a/b/c/d `pwd` "B0.2 test framework sanity check"
# 3 dots may occur in a file name
cd /test && rm -rf *
echo "bar" >foo...bar
asserteq "bar" "`cat foo...bar`" "B0.3 test framework sanity check"
mkdir -p src/a/b/c/d
asserteq "bar" "`cat .../foo...bar`" "B5 must find file in the current
directory"
cd src
asserteq "bar" "`cat .../foo...bar`" "B6 must find file in the parent
directory"
cd a/
asserteq "bar" "`cat .../foo...bar`" "B7 must find file in the parent's
parent directory"
cd b/c/d
asserteq "bar" "`cat .../foo...bar`" "B8 must find file in any ancestor
directory"
asserteq /test/src/a/b/c/d `pwd` "B0.4 test framework sanity check"
```
#### Requirement C: must work for sequences of directory and file names
Desired: use `.../app/build.gradle` to denote the nearest
file `build.gradle` in the directory app somewhere above in the tree.
The file name may not exist, e.g. for `mkdir -P .../app/build/arm`
only `app` must exist.
What we have now: we use `../../../../../../../app/build.gradle` and
we must count the dots
```
cd /test && rm -rf *
echo "bar" >foo
mkdir -p app/a/b/c/d
echo "qux" >app/foo
mkdir -p lib/p/q/r/s
echo "corge" >lib/foo
cd app/a/b/c/d
asserteq "qux" "`cat .../app/foo`" "C1 must find file in ancestor
directory"
asserteq "corge" "`cat .../lib/foo`" "C2 must find file in ancestor's
sibling directory"
asserteq "bar" "`cat .../test/foo`" "C3 find file in the 'project root'
by project dir name"
asserteq "bar" "`cat .../lib/../foo`" "C4 find file in the 'project
root' knowing it's the parent of some other module"
asserteq "bar" "`cat .../app/../foo`" "C5 find file in the 'project
root' knowing it's the parent of our module"
asserteq "corge" "`cat .../app/../lib/foo`" "C6 first overcomplicated
way to find the file"
asserteq "qux" "`cat .../lib/../app/foo`" "C6 second overcomplicated
way to find the file"
echo baaz>.../app/baz
asserteq "baaz" "`cat /test/app/baz`" "C7 must be able to create a
file"
echo grault>.../lib/p/q/r/s/fooo
asserteq "grault" "`cat /test/lib/p/q/r/s/fooo`" "C8 must be able to
create a file deep in the tree"
mkdir -p .../app/build/generated
cd .../app/build/generated
asserteq /test/app/build/generated `pwd` "C9 must be able to create a
directory"
```
#### Requirement D: two dots `../` work both after directory names and
file names (at least if the file name follows three dots).
Requirement D example: `gradlew.bat` is not a directory, but two dots
must work. `.../gradlew.bat/..` means the directory
where `gradlew.bat` is located.
Desired: use `.../gradlew.bat/../build.gradle.kts` to denote the
file `build.gradle.kts` in the ancestor directory containing the
nearest file `gradlew.bat`
What we have now: we use `../../../../../../../../build.gradle.kts`
and we must count the dots
```
cd /test && rm -rf *
mkdir -p app/a/b/c/d/e/f/g/h
mkdir -p lib/p/q/r/s/t/u/v/w
echo root_bat>foo.bat
echo root_sh>foo.sh
echo app_bat>app/a/b/c/d/foo.bat
echo lib_bat>lib/p/q/r/s/foo.bat
cd app/a/b/c/d/e/f/g/h
asserteq /test/app/a/b/c/d/e/f/g/h `pwd` "D0.1 sanity check"
asserteq "app_bat" "`cat .../foo.bat`" "D1 can access the nearest file"
asserteq "root_bat" "`cat .../foo.bat/../../.../foo.bat`" "D2 can
access the 2nd nearest file via ../../..."
asserteq "root_bat" "`cat .../foo.bat/../..../foo.bat`" "D3 can access
the 2nd nearest file via ../...."
asserteq "root_bat" "`cat .../foo.bat/...../foo.bat`" "D3 can access
the 2nd nearest file via ....."
asserteq "root_bat" "`cat .../foo.sh/../foo.bat`" "D4 can access the
project root file via .."
asserteq "root_bat" "`cat .../foo.sh/.../foo.bat`" "D5 can access the
project root file via ..."
```
#### Requirement E: no infinite loop at root.
```
cd /test && rm -rf *
mkdir -p a/b/c/d
cd /test/a/b/c/d
cd /.../test # must terminate
asserteq /test `pwd` "E1 /.../ means /"
cd /
cd /.../test # must terminate
asserteq /test `pwd` "E2 /.../ means /"
# cd /.../ must result in an error
cd /.../ 2>/test/err && echo y>/test/res || echo n>/test/res
asserteq "n" "`cat /test/res`" "E3 /.../ not followed by a filename is
invalid"
```
#### Requirement F: reserved syntax
```
cd /test && rm -rf *
mkdir -p a/a/a/a/a/a
cd a/a
cd a/a/.../ 2>/test/err && echo y>/test/res || echo n>/test/res
asserteq "n" "`cat /test/res`" "F1 /.../ not followed by a filename is
invalid"
rm /test/res
cd a/a/... 2>/test/err && echo y>/test/res || echo n>/test/res
asserteq "n" "`cat /test/res`" "F2 /... not followed by a filename is
invalid"
rm /test/res
cd a/a/.../../ 2>/test/err && echo y>/test/res || echo n>/test/res
asserteq "n" "`cat /test/res`" "F3 /.../ not followed by a filename is
invalid"
rm /test/res
cd a/a/.../.. 2>/test/err && echo y>/test/res || echo n>/test/res
asserteq "n" "`cat /test/res`" "F4 /.../ not followed by a filename is
invalid"
rm /test/res
cd a/a/..././ 2>/test/err && echo y>/test/res || echo n>/test/res
asserteq "n" "`cat /test/res`" "F5 /.../ not followed by a filename is
invalid"
rm /test/res
cd a/a/.../. 2>/test/err && echo y>/test/res || echo n>/test/res
asserteq "n" "`cat /test/res`" "F6 /.../ not followed by a filename is
invalid"
rm /test/res
cd a/a/.../../a 2>/test/err && echo y>/test/res || echo n>/test/res
asserteq "n" "`cat /test/res`" "F7 /.../ not followed by a filename is
invalid"
rm /test/res
cd a/a/..././a 2>/test/err && echo y>/test/res || echo n>/test/res
asserteq "n" "`cat /test/res`" "F8 /.../ not followed by a filename is
invalid"
```
#### Requirement G: advanced usage
```
cd /test && rm -rf *
mkdir -p src/a/b/c/d/tao
mkdir tao
mkdir -p .../src/p/q/r/s/t/u/v
echo foo>.../src/p/q/r/s/bar
cd .../src/p/q/r/s
asserteq /test/src/p/q/r/s `pwd` "G1 must allow multiple file names
after slash in mkdir -p"
asserteq "foo" "`cat bar`" "G2 must allow multiple file names after
slash in dest file path"
cd t
asserteq "foo" "`cat .../bar`" "G3 sanity check"
asserteq "foo" "`cat u/v/.../bar`" "G4 must allow multiple file names
before three dots"
cd /test
asserteq "foo" "`cat /src/p/q/r/s/t/u/v/.../bar`" "G5 must allow
multiple file names before three dots"
cd /test/src/a/b/c/
asserteq "foo" "`cat d/tao/.../src/p/q/r/s/t/u/v/.../bar`" "G6 must
allow multiple file names and multiple three-dots"
```
## 4 Functionality reserved for future versions
4.1 Glob patterns after `.../`
4.2 patterns for matching paths rather than file
names: `.../.[./foo/bar.txt/.].` -- the nearest directory where the
path `foo/bar.txt` denotes an existing file. For such functionality we
need a pair of unused brackets, which is a problem (here I use `.[.`
and `.].`, but that's rather ugly).
## Appendix 1: Poor man's three dots
Poor man's `...`. Does NOT support all of the above features, and does
NOT implement the proposed syntax. But it does find filename in
ancestor directories. Used as `$(... filename)`, which is clumsy.
```
$ cat `which ...`
#!/bin/bash
path=`pwd`
if [ ! -z "$1" ]; then
while : ; do
if [ -e "$path/$1" ] ; then realpath "$path/$1"; break; fi
if [ $path = / ]; then break; fi
path=`dirname $path`
done
fi
```
Example: `$(... gradlew) build`
But `cd $(... gradlew)/..` results in a "Not a directory" error (the
gradle wrapper is a file). The `dirname`-based
equivalent `cd $(dirname $(... gradlew))` of course works as expected.
Comparison (currently achievable vs proposed):
```
`... gradlew` build VS .../gradlew build
cd $(dirname $(... gradlew)) VS cd .../gradlew/..
```
## Appendix 2: the same functionality in Gradle/Groovy
Build systems like Gragle would also benefit from both the three dots
syntax and the very idea that such ancestor tree search is possible.
For example, this a build script from a real `build.gradle`. The
functionality of three dots is implemented with `findAbove()` (believe
me, it was quite a code golf to make its definition short enough):
```Groovy
buildscript {
def findAbove = { String path ->
File res, cur = file('..');
while(cur && !(res = new File(cur, path)).exists()) { cur =
cur.parentFile };
if (!cur) { throw new IllegalArgumentException("'" + path + "'
is not found in ancestor directories") };
res
}
dependencies{
classpath fileTree(dir: findAbove('anotherProject/build/libs'),
include: ['*.jar'])
}
}
```
With the proposed three dots notation, it would be just:
```Groovy
buildscript {
dependencies{
classpath fileTree(dir: '.../anotherProject/build/libs',
include: ['*.jar'])
}
}
```