Implement a UNIX file system and command line parser

Python 2, 358 ... 280 277 bytes

thanks to randomdude999 for -3 bytes and a bugfix.

Input is a list of commands, where each command is represented by a 2-tuple as (cmd, args). Test cases can be transformed using this Retina program.

K=T={}
for c,a in input():
 try:exec"T[a]=1|x=a<'.';if x or T[a]<2:del T[a[3*x:]]|T[a]=T.get(a,{'..':T})|E=T\nfor p in a.split('/'):E=E[p]\nT=E".split('|')[hash(c)%6]
 except:1
def p(t,i):
 for k in sorted(t,cmp,t.get,1):
	if'..'<k:print i+k;t[k]>1!=p(t[k],i+' ')
p({'/':K},'')

Try it online!

Explanation

K=T={}

The file system is represented by a dictionary, where K points to the root directory, and T points to the current directory. Each sub-directory contains a reference to its parent directory under the key '..', which allows for easy execution of cd ... Files are represented by the integer 1.

for c,a in input():
 try:exec"""<touch>|<rm>|<mkdir>|<cd>""".split('|')[hash(c)%4]
 except:1

This loop executes the commands, the right code to execute is selected using the hash of the command (see table below). The execution is wrapped in try/except to catch exceptions that occur in invalid cd and rm calls.

┌───────┬──────────────────────┬─────────────┐
│   cmd │            hash(cmd) │ hash(cmd)%6 │
├───────┼──────────────────────┼─────────────┤
│    cd │    12672076131114255 │           3 │
│ mkdir │ -4476162622565762260 │           2 │
│    rm │    14592087666131641 │           1 │
│ touch │  7353934562497703448 │           0 │
└───────┴──────────────────────┴─────────────┘

# touch
T[a]=1

Creates a new file called a in the current directory.

# rm
x=a<'.'
if x or T[a]<2:del T[a[3*x:]]

If a starts with '-r', x is set to True. If x is True or we want to delete just a file (dicts are greater than integers in Python 2), the object can be deleted.

# mkdir
T[a]=T.get(a,{'..':T})

If the current directory already has an item called a, do nothing. Otherwise create a new subdirectory in the current directory with name a with a parent reference to the current directory.

# cd
E=T
for p in a.split('/'):E=E[p]
T=E

If p is equal to '..', E['..'] points to the parent directory of E. Otherwise E[p] is the subdirectory p in E. The current directory is only updated if all steps have completed without error.

# Function that formats and prints the file system
# t - dictionary representing a part of the file system
# i - current indentation
def p(t,i):
 # Iterate over the keys sorted ...
 # ... on the values, where dicts (directories) ...
 # ... are larger than `1` (files) ...
 # ... and reverse
 for k in sorted(t,cmp,t.get,1):
  # if k is not 0 (a parent reference) ...
  # print the name of k ...
  # and, if k is a directory, call p recursively
  if k:print i+k;t[k]>1!=p(t[k],i+' ')

Bash, 133 86 bytes

(for i;{
$i
[[ $PWD =~ , ]]||cd ->~/e
})
tree --dirsfirst|sed '$d;s#[^0-Z.]# #g;1c /'

-2 bytes thanks to @Dom Hastings (removing spaces around ||)

-6 bytes thanks to @Dom Hastings (removing eval before $i and using # as a sed delimiter)

-12 bytes thanks to @ilkkachu (combining the seds).

-5 bytes thanks to @chepner (=~, $PWD and sed c command)

Takes input where each argument is a command, e.g. script 'mkdir A' 'cd A' 'touch B'

Must be called from an empty directory with name containing ,, such that this directory is the only directory containing , on the system.

The code itself is 85 bytes, +1 byte for specifying the directory name.

Try it online!.

How it Works

(         # start a subshell
for i;do  # for each argument
 $i          # run that command (rm [-r], touch, and mkdir 
             # behave exactly as specified)
             # unfortunately cd can leave the directory, so...
 if [[ $PWD != *,* ]];then  # if we left the directory
                              # (i.e. the directory now no longer contains a comma)
    cd - > ~/e                     # cd to the directory from before the command
                                   # if this is successful, it outputs the new directory to stdout
                                   # so, redirect stdout to a file we can edit
                                   # piping to : didn't work without more bytes
                                   # It would be nice to not have to do this, but 
                                   # redirecting the final `tree` output to a file included that file half the time 
 fi
done
) # end subshell, returning to the initial directory (corresponding to '/')
tree --dirsfirst  # tree does most of the work for us
                  # outputs nearly the desired output, but it looks like
                  # .
                  # ├── A
                  # │   └── B.txt
                  # └── F
                  # 2 directories, 1 file

 | sed '
   $d;              # remove the last line ("2 directories, 1 file")
   s#[^0-Z.]# #g;   # replace all characters that are not digits, letters, or '.' with a space
   1c /             # replace the initial '.' with a '/'
 '

JavaScript (ES6),  268 265 254  248 bytes

Expects an array of strings. Returns a single linefeed-separated string.

a=>a.map(o=r=s=>([[c],s,e]=s.split` `,c>'m'?c>r?o[s]=1:o[e||+o[s]&&s]=0:c<'m'?o=s.split`/`.every(s=>o=o[s]-2?0:o[s],q=o)?o:q:o[s]=o[s]||{'..':o}))&(g=(o,i)=>[0,1].map(t=>{for(k in o)(v=o[k],t?v^1:v-2|k<S)||(S+=i+k,t||g(v,i+' '))}))(r,`
 `,S=`/`)||S

Try it online!

How?

Part 1: parse the commands and build the tree

The file tree is described by an object whose keys are the file names and whose values are:

  • 0 for a deleted entry
  • 1 for a file
  • another object for a directory

Each directory (except the root) contains a default .. entry pointing to the parent directory.

a.map(                   // main loop
  o =                    // o is the current object
  r =                    // r is the root object
  s => (                 // for each string s in a[]:
    [[c], s, e] =        //   split it into c = first character of the command,
      s.split` `,        //   s = first argument, e = second argument
    c > 'm' ?            //   if c is greater than 'm':
      c > r ?            //     if c is greater than 's':
        o[s] = 1         //       touch: create a file whose name is s
      :                  //     else:
        o[               //       rm:
          e ||           //         use e if it exists (meaning that -r was used)
          +o[s] && s     //         or use s if o[s] is a file
        ] = 0            //       mark this entry as deleted
    :                    //   else:
      c < 'm' ?          //     if c is less than 'm':
        o =              //       cd:
          s.split`/`     //         split the path
          .every(s =>    //         for each string s in the path:
            o =          //           update o:
              o[s] - 2 ? //             if o is a file or a deleted entry:
                0        //               abort
              :          //             else:
                o[s],    //               update o to o[s] (may be undefined)
            q = o        //           q = backup of o
          ) ?            //         if all entries were truthy:
            o            //           confirm the update
          :              //         else:
            q            //           restore o to q
      :                  //     else:
        o[s] = o[s] ||   //       mkdir: create a directory whose name is s,
               {'..': o} //       provided that it doesn't already exist
  )                      //
)                        // end of map()

Part 2: build the output string

( g =                    // g is a recursive function taking:
  (o, i) =>              //   o = current object, i = indentation string
  [0, 1].map(t => {      //   for t = 0 and t = 1:
    for(k in o)          //     for each key k in o:
      (                  //
        v = o[k],        //       v = value
        t ?              //       if we are listing files:
          v ^ 1          //         abort if v is not equal to 1
        :                //       else (listing directories):
          v - 2 |        //         abort if v is a file or a deleted entry
          k < S          //         or the directory name is '..'
      ) || (             //       if the above test was falsy:
        S +=             //         append to S:
          i + k,         //           indentation + key
        t ||             //       if we are listing directories:
          g(v, i + ' ')  //         do a recursive call
      )                  //     implicit end of for()
  })                     //   end of map()
)(r, `\n `, S = `/`)     // initial call to g