Abstract 1 Introduction 2 Preliminaries 3 Turing Completeness of find + mkdir 4 Turing Completeness of GNU find (Standalone) 5 Turing Completeness of find + mkdir without Back-references (Sketch) References Appendix A Turing Completeness of find + mkdir without Back-references Appendix B Toy Examples

Turing Completeness of GNU find: From mkdir-Assisted Loops to Standalone Computation

Keigo Oka ORCID Google, Tokyo, Japan
Abstract

The Unix command find is among the first commands taught to beginners, yet remains indispensable for experienced engineers. In this paper, we demonstrate that find possesses unexpected computational power, establishing three Turing completeness results using the GNU implementation (a standard in Linux distributions). (1) find + mkdir is Turing complete. By encoding computational states as directory paths and using regex back-references to copy substrings, we simulate 2-tag systems using only the find and mkdir executables. (2) GNU find 4.9.0+ alone is Turing complete: by reading and writing to files during traversal, we simulate a two-counter machine without mkdir. (3) find + mkdir without regex back-references is still Turing complete: by a trick of encoding regex patterns directly into directory names, we achieve the same power.

These results place find among the “surprisingly Turing-complete” systems, highlighting the hidden complexity within seemingly simple standard utilities.

Keywords and phrases:
Turing completeness, GNU find, tag system, counter machine
Funding:
Keigo Oka: Work done in a personal capacity.
Copyright and License:
[Uncaptioned image] © Keigo Oka; licensed under Creative Commons License CC-BY 4.0
2012 ACM Subject Classification:
Software and its engineering Command and control languages
; Theory of computation Computability
Related Version:
ArXiv Version: https://arxiv.org/abs/2602.20762
Supplementary Material:
Software  (Source Code): https://github.com/ogiekako/pub_find_mkdir
  archived at Software Heritage Logo swh:1:dir:e33cf38786da2c414b41c2fab2728a401c5fda32
Editor:
John Iacono

1 Introduction

The Unix command find is among the first utilities introduced to students and system administrators. While find is designed to search for files in a directory hierarchy, its recursive traversal with the ability to execute commands opens the door to complex behaviors. In this paper, we demonstrate that find possesses unexpected computational power.111A preliminary version of this work appeared on the author’s blog [11] and attracted considerable interest [7].

Foundational work by Minsky [8] and Post [12] established that very simple systems, such as tag systems and counter machines, are capable of universal computation. This insight explains why “accidental” Turing completeness is a recurring theme in computer science: systems designed for specific, limited purposes often turn out to have enough power to perform arbitrary computations. Famous examples include the stream editor sed [3, 2], the x86 mov instruction [5], and even the rules of Magic: The Gathering [4]. In security research, such emergent computational power has been termed “weird machines” [1].

1.1 Contributions

In this paper, we establish three Turing completeness results. Given any instance of a known universal computational model, we show how to construct a fixed sequence of commands (an execution in the sense of Definition 1) whose behavior faithfully simulates that instance. Conversely, execution of any sequence of find and mkdir commands on a file system can clearly be simulated by a Turing machine, so we obtain Turing equivalence; however, we focus on the more surprising direction.

  1. 1.

    GNU find + mkdir is Turing complete (Section 3): By encoding computational states as directory paths and using regex back-references to copy substrings, we simulate 2-tag systems. We use options added in GNU find 4.2.12, which was released in 2005.

  2. 2.

    GNU find alone is Turing complete (Section 4): By reading and writing to files during traversal, we simulate a two-counter machine without mkdir. This construction utilizes the -files0-from option added in GNU find 4.9.0, which was released in 2022.

  3. 3.

    GNU find + mkdir without regex back-references is Turing complete (Section 5 (sketch), Appendix A): By a technique of encoding regex patterns directly into directory names, we achieve the same power even without regex back-references.

Our constructions use O(1)-length path components (hence do not assume an increased NAME_MAX). In practice, resource limits such as storage capacity, maximum path length (PATH_MAX, typically 4096 bytes), and finite inodes cap the number of simulation steps that can be realized on a given machine.

These results place find among the “surprisingly Turing-complete” systems, highlighting the hidden complexity within seemingly simple standard utilities.

We implement our constructions and confirm them on toy examples (Appendix B).

1.2 Applicability and Standards

We restrict our attention to GNU find, because (1) it is standard in Linux distributions, and (2) our constructions rely on options that are GNU extensions. While the options we use for find + mkdir are also found in BSD find (macOS/FreeBSD), subtle runtime differences prevent our constructions from working as-is, though they may work with some modifications.

Table 1 summarizes the Turing completeness results established in this paper.

Table 1: Turing completeness results for find.
find Implementation Key features Model Turing complete Notes
GNU (4.9.0) -files0-from find only Yes Sec. 4
GNU (4.2.12) -regex -execdir find + mkdir Yes Sec. 3
GNU (4.2.12) -regex -execdir find + mkdir Yes Sec. 5
POSIX specification - find + mkdir Unknown No -regex
Proven even without using regex back-references.

The question of whether similar computational universality exists in other implementations, such as BSD, BusyBox, and uutils find, remains an intriguing avenue for future research.

2 Preliminaries

To establish that computation arises solely from find and mkdir, we define the following execution model.

Definition 1.

Let ={find,mkdir} (for the find + mkdir system) or ={find} (for the find system) be the set of permissible binaries. Fix a fresh, writable working directory “.” that is initially empty.

An execution is a finite sequence of command invocations C0,,Cm1, where each Ci is an argument vector (program name and its arguments) that is fully determined before the execution begins – no command’s arguments depend on the output of a previous command. The execution is valid if and only if every program executed during the entire run (including programs spawned indirectly via -exec or -execdir actions) is a binary in . The result of the execution is the concatenation of the standard output of the commands in execution order. We say the execution halts if all commands in the sequence terminate.

This condition can be intuitively understood through a security lens: consider a restricted, shell-free system (e.g., a minimal container) that provides only the binaries in . An adversary may invoke a constant number of these commands. Although -exec and -execdir can in general launch arbitrary programs, in our model only are available, so the adversary cannot exploit these actions to run anything beyond find and mkdir. What we prove is that even under this restriction, the adversary can force the system to perform arbitrary computations.

We clarify the resource model used throughout this paper.

Definition 2 (Resource model).

We work in an idealized file-system model with unbounded capacity: there is no fixed upper bound on the number of directory entries that can be created, no bound on the total path length, and no bound on the amount of data stored in file contents. We do not, however, assume unbounded length of a single path component (i.e., a file or directory name): our constructions use only O(1)-length path components and therefore do not rely on increasing NAME_MAX (the operating system’s limit on path component length, typically 255 bytes on Linux), which we treat as a fixed finite constant.

Before presenting our constructions, we review the key concepts of GNU find [6] and mkdir.

The find utility recursively traverses a directory tree, evaluating an expression for each file visited. This expression functions as a domain-specific language for file filtering and manipulation. The expression is composed of:

  • Tests: Predicates that return true or false based on file attributes (e.g., -name, -empty, -regex).

  • Actions: Operations that perform side effects (e.g., -exec, -delete) or control traversal (e.g., -prune, -quit).

  • Operators: Logical connectives -and (implicit), -or (-o), and -not (!), where and/or evaluations are short-circuiting (right-hand side is evaluated only if needed).

Of particular importance are the actions -exec and -execdir. They accept a command template terminated by ;, where the string {} is replaced by the current file path. The specified (either find or mkdir) command is executed directly (via fork and exec) without invoking a shell, adhering to our execution model. The action is evaluated to true only if the exit status of the command is 0. -exec runs the command in the same directory as the outer find, and -execdir runs the command in the directory holding the current file, with {} replaced with the filepath relative from the directory. This locality of -execdir is vital for our scale-independent loop construction (Section 3.1), as it allows us to avoid the PATH_MAX limit.

We note that nesting -exec within -exec (e.g., -exec find ... -exec ... ; ;) is syntactically invalid: the outer find treats the first ; as the terminator for its own -exec, so the inner -exec has no terminator. The same applies to -execdir. This restriction significantly constrains our design.

The directory traversal order of find is pre-order by default, meaning a directory is visited before its children. This can be changed to post-order with the global option -depth. Table 2 summarizes the find options used in this paper.

mkdir path creates a directory at the path. If its parent directory does not exist, creation fails and the command exits with a non-zero status. mkdir -p path creates the directory and its parent directories if they do not exist, and exits with a zero status. These functionalities are specified in POSIX and consistent across platforms. Our construction never relies on creation failure due to permission issues.

Table 2: Summary of GNU find options used in this paper. Operators below ( expr ) are listed in order of decreasing precedence.
Option/Operator Description Added in
-true True. -
-empty True if the file/directory is empty. -
-name pattern True if the file’s basename matches the pattern. -
-size nc True if the file size is n bytes. -
-regex pattern True if the file path matches the pattern. -
-exec cmd ; Executes cmd with {} replaced by the path. True if the exit status is 0. -
-execdir cmd ; Executes cmd in the file’s directory with {} replaced by the relative path. True if the exit status is 0. 4.2.12
-prune True. Do not descend into the matched directory. -
-quit Terminates the entire find execution. -
-delete Removes the matched file or (if empty) directory. Implies -depth. True if removal succeeds. 4.2.3
-printf fmt True. Print formatted output to standard output. -
-fprintf file fmt True. Write formatted output to file. -
-depth Global option to do post-order traversal. -
-files0-from file Global option to read starting points from file (NUL-separated) instead of the command line. 4.9.0
( expr ) True if expr is true. Force precedence. -
! expr Not (true if expr is false). -
expr1 expr2 And. expr2 is not evaluated if expr1 is false. -
expr1 -o expr2 Or. expr2 is not evaluated if expr1 is true. -

3 Turing Completeness of find + mkdir

In this section, we show the following theorem.

Theorem 3.

GNU find + mkdir is Turing complete for find version 4.2.12 (as of this writing when 4.10.0 is the latest), assuming the resource model in Definition 2.

3.1 Loop Construction

The foundation of our constructions is the ability to create an infinite loop using only find and mkdir. To achieve this, we rely on a specific property of find: child directories created during the traversal of a directory become visible and are subsequently visited in the same execution, provided the traversal is pre-order (the default). When find visits a directory D and an action creates a new subdirectory C inside D, C is visible to the subsequent directory enumeration and will be visited in the same find execution. While POSIX leaves this behavior unspecified222“If a file is removed from or added to the directory hierarchy being searched it is unspecified whether or not find includes that file in its search.” [14], this behavior is consistent across GNU find versions333Verified on GNU find 4.10.0 (Debian) and 4.5.11 (CentOS 7)..

The following code implements an infinite loop. In our code snippets, we do not escape characters like ; (e.g., as \;), which is typically required by a shell. This is to signify shell independence. Note that proper escaping is necessary to execute these snippets in a shell.

mkdir x
find x -execdir mkdir {}/x ;

This code creates x, then find visits x and executes mkdir command in the directory . (the parent of x) with the argument ./x/x, because {} is substituted with ./x, that is the relative path of x from . . This creates x/x. Due to the aforementioned property, find then visits x/x, and it then creates x/x/x, and so on indefinitely.

We use -execdir, which was added in GNU find 4.2.12. While strictly speaking our resource model (Definition 2) does not forbid arbitrary long arguments, using -execdir is practically crucial. If we use -exec, find passes the path of the current file to the command. As the directory structure grows deeper, this path eventually exceeds PATH_MAX (typically 4096 bytes), causing mkdir to fail and find to halt. -execdir avoids this by executing the command in the subdirectory containing the current file, allowing us to pass an argument of constant length.

3.2 Tag System Implementation

We prove that find + mkdir is Turing complete by simulating a 2-tag system [12]. The following is a definition of a 2-tag system equivalent to the one given by Rogozhin [13].

Definition 4.

A 2-tag system is defined by a pair (Σ,P), where Σ is a finite alphabet containing a halting symbol HΣ, and P is a (partial) map with domain Σ{H} and codomain Σ. A word is a finite string over Σ. The computation starts with an initial word w1Σ, and proceeds iteratively. In the i-th iteration, with wi=xi,1xi,k, the computation halts if k<2 or xi,1=H. Otherwise, the next word is wi+1=xi,3xi,kP(xi,1). If the computation halts at wt, wt is called the halting word, and is the output of the computation.

It is known that 2-tag systems are computationally universal. Specifically, we can simulate an arbitrary 2-symbol m-state Turing machine (TM(m,2)) using a 2-tag system. Combining this reduction with the known existence of a universal Turing machine with 18 states and 2 symbols (TM(18,2)) [10], De Mol [9] constructed a universal 2-tag system with 576 symbols (TS(576,2)).

We prove Turing completeness of find + mkdir by simulating an arbitrary 2-tag system, applying the construction to this 576-symbol universal instance. The high-level idea is:

  1. 1.

    Encode each tag-system symbol as a two-letter directory name (e.g., /ab), representing the sequence of words as a growing, nested directory path.

  2. 2.

    To compute the next word, iteratively copy the remaining suffix of the previous word symbol-by-symbol: use a regex with a back-reference to verify the progress and locate the next symbol, then use -execdir mkdir to append it to the path.

  3. 3.

    Once the suffix is fully copied, use regex matching to identify the first symbol of the previous word, and append its corresponding production string to complete the iteration.

We will encode the input word to an ASCII string and decode the halting word from an ASCII string. Since this mapping is straightforward (clearly computable by a deterministic finite state transducer), we omit a formal proof for the validity of the encoding. We use typewriter font to denote string literals, concatenation (written by placing strings next to each other), and ε for the empty string.

Let μ=576, and Σ={s1,,sμ,sμ+1=H}. We encode each symbol as a directory name: let A be a set of lowercase ASCII letters, and choose distinct strings s1,,sμ+1A2 (possible because 577<262=676). Define σk=/sk, where / is the directory separator. The encoding of a symbol sk is thus a three-character string like /ab. We let Σ={σ1,,σμ,σμ+1=η} and ϕ:ΣΣ be a function that maps si to σi. We also define Φ:ΣΣ so that Φ(ε)=ε and Φ(s1sk)=Φ(s1sk1)ϕ(sk) for k1. We define π(si)=Φ(P(si)) for iμ.

We first run the following command to embed the initial word w1. We use spaces to separate command arguments, distinguishing them from concatenation.

1mkdir -p _Φ(w1)/_

The idea for the following computation is to iteratively append the file path representing the next word to it using /_ as a separator. During the computation, the working directory should contain exactly one directory _, each of whose descendants is a directory containing at most one directory. We call the path to the empty directory at a point of the computation the state of the system at that point. After the computation halts, the state of the system should be _Φ(w1)/_Φ(w2)/_/_Φ(wt)/_, and during the computation the state should be a prefix of it. Let Θi,j be the following state. Our initial state is Θ1,0 and the last state will be Θt,0.

Θi,j=__Φ(wi)/_ϕ(xi+1,1)ϕ(xi+1,j)

At Θi,j, if j=0 and wi is a halting word, we stop the computation. Otherwise, (1) if ϕ(xi+1,1)ϕ(xi+1,j) equals ϕ(xi,3)ϕ(xi,j+2) and xi,j+3 exists, we append ϕ(xi,j+3) to the state, and (2) otherwise (if we have copied the last alphabet of the previous word), we append π(xi,1)/_ to the state.

Let the regex pattern λ=/[a-z][a-z]. Since [a-z] matches any lowercase ASCII letter but not / or _, λ matches exactly the strings in Σ (encoded symbols). Let Λ=\(λ\)* be a regex that matches any (possibly empty) sequence of encoded symbols. \( and \) are escaped because a backslash before parentheses is required by find’s default regex type (emacs regex). The same applies for \| (OR operator) below. Define the following regex patterns:

αk =.*_λλ\(Λ\)σkΛ/_\1
βk =.*_σkΛ/_Λ
γ =.*_\(\|λ\|ηΛ\)/_

We run the following as the second command. Newlines are for readability and have the same meaning as spaces.

1find _ -empty (
2 -regex γ -quit -o
3 -regex α1 -execdir mkdir {}σ1 ; -o
4
5 -regex αμ+1 -execdir mkdir {}σμ+1 ; -o
6 -regex β1 -execdir mkdir -p {}π(s1)/_ ; -o
7
8 -regex βμ -execdir mkdir -p {}π(sμ)/_ ; -o
9 -printf unreachable
10)

Finally we run the following command to output the encoded result.

1find _ -depth ! -empty -name _ -execdir find _ ! -name _ -printf /%f ; -quit

We show that the output of the last command is Φ(wt) if the original 2-tag system halts with wt, and the second command does not halt if the original does not halt, proving Theorem 3.

Proof.

Let (r) be the language denoted by the regex r. Remember that Σ(λ),Σ(Λ) and any string including _ or ending with / is not in (λ) nor (Λ). {σk}=(σk) and {η}=(η). We have σk=ϕ(sk) and Θi,j=_ϕ(xi,1)ϕ(xi,|wi|)/_ϕ(xi+1,1)ϕ(xi+1,j), where xi+1,j+2=xi,j for 1j|wi|2.

We prove that the state is Θt,0 if the second command halts by induction. Let us call the expression in the parentheses the main expression. As the base case, find visits Θ1,0 and evaluates the main expression for the first time because of the -empty predicate. Assume the current state is Θi,j and find visits it, evaluating the main expression. By construction, γ matches if and only if j=0 and |wi|<2 or xi,1=H, meaning wi is the halting word. If j<|wi|2 and xi,j+3=sk, Θi,j(αk) because σk matches with ϕ(sk), and \(Λ\) and \1 match with ϕ(xi,3)ϕ(xi,j+2) and ϕ(xi+1,1)ϕ(xi+1,j) which are the same string. The converse can be said because the _ before \1 combined with the fact the same string should match Λ constrains it to match ϕ(xi+1,1)ϕ(xi+1,j) and σk only matches with ϕ(sk). Provided j=|wi|2, Θi,j(βk) if and only if ϕ(xi,1)=σk by construction. These enforce the main expression to work as follows: (1) If (i,j)=(t,0), it halts the command. (2) Otherwise if j<|wi|2, it executes mkdir {}ϕ(xi,j+3) turning the state into Φi,j+1. (3) Otherwise at j=|wi|2, it executes mkdir -p {}π(xi,1)/_ turning the state into Φi+1,0. The newly created state is subsequently visited due to the said GNU find property, so the induction completes.

The third command visits the state in post-order (-depth), matches the first non-empty _, which is the second-to-last _ directory (say base), executes the inner find for the base directory, which visits its descendants in pre-order, printing the name of each directory prefixing / (due to -printf /%f) unless the name is _ (due to ! -name _), effectively outputting Φ(wt), then quit.

4 Turing Completeness of GNU find (Standalone)

We prove that GNU find version 4.9.0 or later is Turing complete without mkdir. We prove this by simulating Minsky’s program machine [8].

Theorem 5.

GNU find is Turing complete for find version 4.9.0 (as of this writing when 4.10.0 is the latest), assuming the resource model in Definition 2.

The high-level idea is as follows. We represent two counters as files whose byte-length encodes their values. The key mechanism is the -files0-from option, which lets find read starting points from a file: if the file contains n copies of the string .nul, find visits the current directory . exactly n times, allowing us to repeat an action n times. By reading and writing such files during traversal, we simulate the increment, decrement, and conditional-jump instructions of a two-counter program machine.

4.1 Loop Construction

The foundation of our constructions is the ability to create an infinite loop using only find. We utilize -files0-from file, which lets find read NUL-separated starting points from a file rather than the command line (where nul is the ASCII null character). find’s -fprintf can write nul to a file provided \0 verbatim. By updating this file while the find command is running, we can supply an infinite stream of starting points, thereby creating a loop.

We first introduce a way of encoding a non-negative integer as a file. Let ι=.nul (a two-byte string). If the content of a file f is the string ι repeated n times (i.e., 2n bytes), we say the value of f is n. For example, a file of value 3 contains the 6-byte string .nul .nul .nul. If f does not exist, its value is 0; in other cases the value is undefined. We will use files a, b, s, t and represent their values with a,b,s,t respectively. We use t for temporary storage. We construct our program so that throughout the computation a,b,s are always defined.

Notice that the code like find -files0-from s -prune expr evaluates expr s times, and it exits with non-zero exit code if s=0 (s does not exist).

For loop construction, we first initialize s (for stream) with 1.

1find -fprintf s .\0 -quit

We define an expression which increments s when evaluated as follows, and call this inc(s). We similarly define inc(a) and inc(b) for later use, in which s is replaced with a or b.

1( ( -exec find s -quit ;
2-exec find -quit -fprintf first . ;
3-exec find -files0-from s ( -name . -o -name first -delete ) -fprintf t .\0 ;
4-exec find -files0-from t -prune -fprintf s .\0 ;
5-exec find t -delete ; ) -o
6-exec find -fprintf s .\0 -quit ; )

To understand this, remember that the file given to -fprintf is truncated or created to 0 bytes as soon as the find command starts, regardless of whether its predicate ever matches (as documented). Line 1 is evaluated to true if and only if s0. Line 2 (where s>0) creates an empty file first (because of -quit, -fprintf is not evaluated). Line 3 updates t with .nul repeated s+1 times and removes first (first evaluation matches with both . and ./first, removing first with -delete), effectively meaning ts+1. Line 4 updates s with .nul repeated t times (st), and line 5 deletes t (t0). If s=0, line 6 updates s with .nul (s1).

The loop is constructed as follows. Remember we start with s=1.

1find -files0-from s -prune inc(s);

This works because GNU find reads s as a stream. When the first .nul is read, find still has not read the EOF (end of file). It then executes inc(s) to extend the file so that it still has the next .nul, and this process continues indefinitely. The fact that this works depends entirely on the implementation of GNU find, but this has been empirically confirmed on GNU find 4.10.0 and rigorously confirmed to work by reading the source code of GNU find 4.9.0.

4.2 Program Machines

The following definition for 2-counter program machine is equivalent to the one given by Minsky [8]. 2-counter program machines are known to be Turing complete.

Definition 6.

A 2-counter program machine is a finite sequence of instructions P=(I0,,Im1) acting on counters c0,c1. A configuration is (pc,c0,c1){0,,m}×2, where pc=m denotes the halted configuration. The one-step transition relation P is defined for pc<m as follows (with r{0,1} and q{0,,m}):

  • If Ipc=INC(r) then crcr+1 and pcpc+1.

  • If Ipc=DEC(r) then crmax{cr1,0} and pcpc+1.

  • If Ipc=JZ(r,q) then pcq if cr=0, and pcpc+1 otherwise.

  • If Ipc=J(q) then pcq.

An execution of P from an initial configuration (0,c0,c1) is the maximal P chain; it halts if and only if it reaches pc=m, and outputs c0.

To simulate 2-counter program with find, we create expressions to simulate the instructions. We have already seen inc expressions. We define the following expression as dec(a), and dec(b) analogously.

1( ( -exec find a -size 2c -delete ;
2-exec find a -quit ;
3-exec find -quit -fprint first ;
4-exec find -files0-from a -name first -delete -fprintf t skip -o -name t -fprintf t . -o -name . -fprintf t \0 ;
5-exec find -files0-from t -name . -fprintf a .\0 ;
6-exec find t -delete ;
7) -o -true )

When the expression is evaluated, it sets a to max(a1,0) as follows: Line 1 evaluates to true and deletes a (a0) if and only if a=1 (since the file size is 2 bytes, checked via -size 2c). Line 2 evaluates to false if a=0. Line 3 (with a2) creates first. Line 4 sets t to a1, which happens as follows: (1) Due to -fprintf t, t exists as an empty file as soon as the find starts. (2) In the first iteration of . from a, because of -delete implying -depth, ./first (being deleted) and ./t are iterated before . and it causes either .skip nul or skip. nul to be written to t. (3) In the following a1 iterations, since first no longer exists, a1 repeat of .nul is appended to t. In line 5, since neither file .skip nor skip. exists, it is skipped with an error message written to standard error (which we do not use), causing -fprintf a .\0 to be evaluated a1 times (aa1). Line 6 deletes t (t0). Line 7 exists to make dec(a) always true.

The program counter is represented by the file pc, but unlike a and b, we directly encode the value n as a string of n ones (ASCII character 1). For example, the program counter being 3 means pc’s content is 111. pc will be initialized to an empty file with find -quit -fprintf pc not-written, and the program counter will always be defined hereafter. Let 1n denote a string of n ones.

We define the expression j(q) for q{0,,m} as follows. If an empty string is a concern, we can make it -exec find -quit -fprintf pc x ; for q=0. It is clear that it is evaluated to true and sets pc to q.

1-exec find -fprintf pc 1q -quit ;

We define the expression jz(a,q) for q{0,,m} as follows, and jz(b,q) analogously. It is clear that jz(a,q) is evaluated to true and sets pc to q if and only if a=0.

1( ( ! -exec find a -quit ; j(q) ) -o -true )

Finally, we define the expression ispc(q) for q{0,,m1} as follows. Unlike other expressions, it assumes the current file is pc, and is evaluated to true if and only if pc=q. q in the code is substituted to the corresponding decimal number represented in ASCII.

1-size qc

The commands to simulate a given 2-counter program P=(I0,,Im1) and initial configuration (0,c0,c1) is as follows. We let γ0=a and γ1=b.

Our first command to run is find -quit -fprintf pc x, which initializes pc to 0. The next command to run is the following, where inc(a) is repeated c0 times and inc(b) is repeated c1 times. This initializes s, a, and b with 1, c0, and c1 respectively.

1find inc(s) inc(a)  inc(a) inc(b)  inc(b) -quit

The next command is the main loop. Let ϕ be a mapping from {I0,,Im1} to find expressions defined by the following.

ϕ(INC(r)) =inc(γr)
ϕ(DEC(r)) =dec(γr)
ϕ(JZ(r,q)) =jz(γr,q)
ϕ(J(q)) =j(q)

The main loop is as follows.

1find -files0-from s -name pc inc(s) (
2 ispc(0) j(1) ϕ(I0) -o
3
4 ispc(m1) j(m) ϕ(Im1) -o
5 -quit
6)

Finally, the following two commands allow us to directly output the value of a. The first command writes a 1s to count. The second command outputs the byte count of count, which is a.

find -files0-from a -fprintf count 1 -prune
find count -printf %s

We show that by running the above commands from the first to the last, we get the same output as the original 2-counter machine if the machine halts, and it does not halt if the machine does not halt, completing the proof of Theorem 5. It is enough to show that the main loop works as expected.

Proof for Theorem 5.

We call the expression in the parenthesis the main expression. The main loop uses -files0-from s to read starting points from the file s: each .nul entry in s causes find to visit the current directory ., where it finds the file pc and evaluates inc(s) followed by the main expression. Since inc(s) appends a new entry to s before find reaches the end of the file, the loop runs indefinitely (until -quit is evaluated).

We prove the claim by induction on the number of steps t of the program machine. Let (pc(t),c0(t),c1(t)) be the configuration of the program machine at step t. Assume that the values encoded by pc, a, b (pc,a,b) are equal to pc(t),c0(t),c1(t) just before the (t+1)-th evaluation of the main expression. The base case t=0 holds as previously shown.

At the (t+1)-th evaluation, ispc(i) is true if and only if i=pc=pc(t). Therefore, -quit is evaluated if and only if pc=m, in which case both the main loop and the program machine halt, leaving a=c0(t). Otherwise, the program machine executes Ipc, and the find command evaluates j(pc+1) ϕ(Ipc). This sequence updates (pc,a,b) exactly as Ipc updates (pc(t),c0(t),c1(t)), so the new values equal (pc(t+1),c0(t+1),c1(t+1)). Since j(pc+1) ϕ(Ipc) always evaluates to true, the main expression completes (due to the subsequent -o). This completes the proof.

5 Turing Completeness of find + mkdir without Back-references (Sketch)

While Section 3 relies on regex back-references for the “copy” operation, we can achieve Turing completeness without back-references. We present the core ideas for our proof and provide a proof sketch. The full proof is in Appendix A.

To signify non-use of back-references, we will always use -regextype awk global option for any find that uses -regex pattern. The awk regex engine does not support back-references. (It also uses unescaped parentheses ( and ) for grouping, unlike the default emacs regex engine which requires \( and \), though this is not an important advantage for us, and the similar construction should work for any other regex types.)

We build on the idea in Section 3 to simulate 2-tag systems. That is, during the computation, there is only one empty directory under the current working directory, and the path to it represents the state of the computation. The encoded symbols are the strings {σi}. When the computation halts, the final state encodes the sequence of words w1,,wt from left to right, delimited by separator strings. During the computation, the state has the form of Θi,j, which is a prefix of the last state and encodes the status where the word wi is given and the first j letters of the word wi+1 have been computed. xi,j denotes the j-th symbol of wi, and we let ϕ be a function that maps si to σi, where {si} are the symbols of the original tag-system.

We define Σ={σ1,,σμ+1} as before. That is, Σ(/[a-z][a-z]) and σiσj for ij. The main trick is that we use sep=$_)?( as the separator, instead of _. Those are allowed characters for filenames. Note that the letter $ in regex is special in that it only matches the end of the string, and ? indicates one or zero occurrence of the previous atom. This means $_ is a pattern that never matches any string. This choice makes the state Θi,j during the computation look like the following.

Θi,j=$_)?(/$_)?(ϕ(xi,1)ϕ(xi,|wi|)/$_)?(ϕ(xi+1,1)ϕ(xi+1,j)

Let us consider the regex (Θi,j), which should look like this:

($_)?($_)?($_)?(ϕ(xi+1,1)ϕ(xi+1,j))

This (valid) regex matches only ϕ(xi+1,1)ϕ(xi+1,j). The regex obviously matches the string, and conversely, if it matches (because ($_)=), every ()? must be matching with an empty string.

This observation allows us to update the part where we used back-references in the previous proof. That is, at state Θi,j where j<|wi|2, we append ϕ(xi,j+3) to the state without back-references. The idea is (1) we create μ+1 marker files t1,,tμ+1 under the directory that represents the current state, (2) we invoke μ+1 inner find’s so that k-th one’s regex matches with tk’s path only when ϕ(xi,j+3)=σk, deleting the marker file. To construct such a regex, we embed the state into the regex with {} (detailed below). (3) we check marker files and create a corresponding directory if a file is gone, advancing the state.

The regex constructed in the step (2) for k is the following, where λ=/[a-z][a-z] and Λ=(λ)*.

.*sepλλ({})σkΛsepΛ/tk

The state Θi,j has ϕ(xi+1,1)ϕ(xi+1,j) after the separator. The ({}) is expanded to (Θi,j), and as we have seen it matches with and only with ϕ(xi+1,1)ϕ(xi+1,j). We know it is equal to ϕ(xi,3)ϕ(xi,j+2). This ensures that the regex matches if and only if ϕ(xi,j+3)=σk, and the current file is the marker file tk.

We can use the same construction as the previous proof on other parts of the commands, and this completes the proof sketch of the following theorem.

Theorem 7.

GNU find + mkdir is Turing complete without using regex back-references for find version 4.2.12 (as of this writing when 4.10.0 is the latest), assuming the resource model in Definition 2.

References

Appendix A Turing Completeness of find + mkdir without Back-references

We expand on the sketch of the proof in Section 5.

We prove Theorem 7 by simulating 2-tag systems (Definition 4) with find + mkdir without regex back-references. We use the notation in the definition in the following proof. Note that the construction resembles the one in Section 3.

Let μ=576, and Σ={s1,,sμ,sμ+1=H}. Let A be a set of lowercase ASCII letters, and {s1,,sμ,sμ+1}A2 be distinct strings with length 2. We will use -regextype awk for finds with -regex to ensure that regexes are matched without back-references, while similar constructions should work for other regex types.

We define σk=/sk, where / is the ASCII letter for directory separator. We let Σ={σ1,,σμ,σμ+1=η} and ϕ:ΣΣ be a function that maps si to σi. We also define Φ:ΣΣ so that Φ(ε)=ε and Φ(s1sk)=Φ(s1sk1)ϕ(sk) for k1. We define π(si)=Φ(P(si)) for iμ.

Let sep=$_)?( . We first run the following command to embed the initial word.

1mkdir -p sepΦ(w1)/sep

The idea for the following computation is to iteratively append the file path representing the next word to it using sep as a separator. During the computation, the current working directory (CWD) should contain exactly one directory sep, and under sep there is exactly one empty directory. We call the path to the empty directory at a point of the computation the state of the system at that point. After the computation halts, the state of the system should be sepΦ(w1)/sepΦ(w2)/sep/sepΦ(wt)/sep, and during the computation the state should be a prefix of it. Let Θi,j be the following state. Our initial state is Θ1,0 and the last state will be Θt,0.

Θi,j=sepΦ(wi)/sepϕ(xi+1,1)ϕ(xi+1,j)

At Θi,j, if j=0 and wi is a word to halt, we stop the computation. Otherwise, (1) if ϕ(xi,j+3) exists, we append ϕ(xi,j+3) to the state, and (2) otherwise (if we have copied the last alphabet of the previous word), we append π(xi,1)/sep to the state.

Let λ=/[a-z][a-z] be a regex that matches any string in Σ. Let Λ=(λ)* be a regex that matches any repetition of strings in Σ. We also define sep¯=\$_\)\?\( as a string that satisfies (sep¯)={sep}. Define t1=1,t2=2, until tμ+1, which is an ASCII string representing μ+1 in decimal. Define the following strings.

αk =.*sep¯λλ({})σkΛ/sep¯Λ/tk
βk =.*sep¯σkΛ/sep¯Λ
γ =.*sep¯(|λ|ηΛ)/sep¯

We run the following as the second command. Newlines are for readability and have the same meaning as spaces.

1find sep -regextype awk -type d -empty (
2 -regex γ -quit -o Halt check
3 -execdir find -fprint {}/t1 -quit ; Save tk
4
5 -execdir find -fprint {}/tμ+1 -quit ;
6 -exec find sep -regextype awk -type f -regex α1 -delete -quit ; Check αk
7
8 -exec find sep -regextype awk -type f -regex αμ+1 -delete -quit ;
9 (
10 ! -execdir find {}/t1 -quit ; -execdir mkdir {}/σ1 ; -o Append next
11
12 ! -execdir find {}/tμ+1 -quit ; -execdir mkdir {}/σμ+1 ; -o
13 -false
14 ) -o (
15 -regex β1 -execdir mkdir -p {}π(s1)/sep ; -o Transition
16
17 -regex βμ -execdir mkdir -p {}π(sμ)/sep ; -o
18 -printf unreachable
19 ) ,
20 -exec find sep -type f -delete ;
21)

It uses some constructions that are not mentioned in the preliminaries section: -type d and -type f are evaluated to true if and only if the current file is a directory and a regular file, respectively. -fprint file prints the path to the current file to file (while we only use it to create a file). expr1 , expr2 evaluates both expr1 and expr2 and its value becomes the value of expr2. The , has the least precedence (less than -o).

Finally we run the following command to output the encoded result.

1find sep -depth ! -empty -name sep -execdir find sep ! -name sep -printf /%f ; -quit

We show that the output of the last command is Φ(wt) if the original 2-tag system halts with wt, and the second command does not halt if the original does not halt, proving Theorem 7.

Proof.

Remember that Σ(λ),Σ(Λ) and any string including a character in sep or ending with / is not in (λ) nor (Λ). {σk}=(σk) and {η}=(η). We have Θi,j=sepϕ(xi,1)ϕ(xi,|wi|)/sepϕ(xi+1,1)ϕ(xi+1,j), where xi+1,j+2=xi,j for 1j|wi|2.

We prove that the state is Θt,0 if the second command halts by induction. Let us call the expression in the outermost parentheses the main expression. The -type d predicate is evaluated to true if and only if the current file is a directory. This means the expression is evaluated only when the current file is an empty directory. As the base case, find visits Θ1,0 and evaluates the main expression for the first time. Assume the current state is Θi,j and find visits it, evaluating the main expression. We also assume that before the evaluation Θi,j is the only empty directory under CWD, and there is no regular file (that matches -type f) under CWD, calling it the cleanness condition. We will prove that after the evaluation the cleanness condition still holds as well. By construction, γ matches and halts computation if and only if j=0 and |wi|<2 or xi,1=H, meaning wi is the halting word (Line 2). If it does not halt, it creates marker files Θi,j/tk for k=1,,μ+1 (Line 3). Then it evaluates the expressions from line 6. The k-th expression from line 6 executes find for the sep in the CWD. Due to the cleanness condition and -type f, if it matches, it should be one of t1,,tμ+1. Also because αk ends with /tk, it can match with tk only. If the regex matches tk, it means

Θi,j/tk(.*sep¯λλ(Θi,j)σkΛ/sep¯Λ/tk)

meaning

Θi,j(.*sep¯λλ(Θi,j)σkΛ/sep¯Λ)

As explained in Section 5,

((Θi,j))=ϕ(xi+1,1)ϕ(xi+1,j)=ϕ(xi,3)ϕ(xi,j+2)

Therefore, the match happens if and only if σk=ϕ(xi,j+3). That is, tk is deleted if and only if there exists a σk=ϕ(xi,j+3), and no marker files are deleted if there is no such σk, meaning j|wi|2. Whether deletion happens or not, the code block from line 9 is evaluated. This is evaluated to true creating a directory Θi,j+1 if and only if there exists a deleted marker file tk, because the k-th expression’s find exits with non-zero status code only if the starting point Θi,j/tk does not exist. The code block from line 14 is therefore evaluated when j=|wi|2 for the first time for each i. By construction this turns the state into Θi+1,0 (see the proof in Section 3 for details). Finally, because of the , operator, the line -exec find sep -type f -delete ; is always evaluated, deleting all marker files, restoring the cleanness condition.

The third command (if the second command halts) will output Φ(wt) as explained in the proof in Section 3.

Appendix B Toy Examples

B.1 find + mkdir with back-references

We use the following 2-tag system instance as an example444Taken from https://en.wikipedia.org/wiki/Tag_system#Example:_A_simple_2-tag_illustration. The expected output is Hcccccca.

Σ ={a,b,c,H=H}
P(a) =ccbaH
P(b) =cca
P(c) =cc
w1 =baa

Following shell script directly translates the construction in Section 3. If it is run, it outputs the encoded halting word /ad/ac/ac/ac/ac/ac/ac/aa.

1#!/bin/bash
2TMP="$(mktemp-d)"
3cd "$TMP" # For safety.
4# Start of the main program
5
6A=("aa" "ab" "ac" "ad")
7sigma=(/"${A[0]}" /"${A[1]}" /"${A[2]}" /"${A[3]}")
8eta="${sigma[3]}"
9lambda="/[a-z][a-z]"
10Lambda=’\(’"$lambda"’\)*’
11
12pi=(
13 "${sigma[2]}${sigma[2]}${sigma[1]}${sigma[0]}${sigma[3]}"
14 "${sigma[2]}${sigma[2]}${sigma[0]}"
15 "${sigma[2]}${sigma[2]}"
16)
17Phiw1="${sigma[1]}${sigma[0]}${sigma[0]}" # baa
18
19alpha=(
20 ’.*_’"$lambda$lambda"’\(’"$Lambda"’\)’"${sigma[0]}$Lambda"’/_\1’
21 ’.*_’"$lambda$lambda"’\(’"$Lambda"’\)’"${sigma[1]}$Lambda"’/_\1’
22 ’.*_’"$lambda$lambda"’\(’"$Lambda"’\)’"${sigma[2]}$Lambda"’/_\1’
23 ’.*_’"$lambda$lambda"’\(’"$Lambda"’\)’"${sigma[3]}$Lambda"’/_\1’
24)
25beta=(
26 ’.*_’"${sigma[0]}$Lambda"’/_’"$Lambda"
27 ’.*_’"${sigma[1]}$Lambda"’/_’"$Lambda"
28 ’.*_’"${sigma[2]}$Lambda"’/_’"$Lambda"
29)
30gamma=’.*_\(\|’"$lambda"’\|’"$eta$Lambda"’\)/_’
31
32mkdir -p ’_’"$Phiw1"’/_’
33find _ -empty \( \
34 -regex "$gamma" -quit -o \
35 -regex "${alpha[0]}" -execdir mkdir {}"${sigma[0]}" \; -o \
36 -regex "${alpha[1]}" -execdir mkdir {}"${sigma[1]}" \; -o \
37 -regex "${alpha[2]}" -execdir mkdir {}"${sigma[2]}" \; -o \
38 -regex "${alpha[3]}" -execdir mkdir {}"${sigma[3]}" \; -o \
39 -regex "${beta[0]}" -execdir mkdir -p {}"${pi[0]}"/_ \; -o \
40 -regex "${beta[1]}" -execdir mkdir -p {}"${pi[1]}"/_ \; -o \
41 -regex "${beta[2]}" -execdir mkdir -p {}"${pi[2]}"/_ \; -o \
42 -printf unreachable \
43\)
44find _ -depth ! -empty -name _ -execdir find _ ! -name _ -printf /%f \; -quit
45
46# End of the main program
47rm -rf "$TMP"

B.2 GitHub repository

Other toy examples are available at the author’s GitHub repository https://github.com/ogiekako/pub_find_mkdir.