# 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'])

       }

   }

   ```

Reply via email to