Implement a UNIX file system and command line parser

Description

Your task is to implement a simple UNIX command parser and file system. Your program will have to implement a file system that can be modified via commands.

The starting directory of your file system is an empty root directory /, with no subdirectories or files. Your program must be capable of handling the following commands:

Commands

cd <dirpath> - Change the current directory

• <dirpath> will be a list of directory names or "..", seperated by "/"
• e.g. cd ../folder1/folder2 means to navigate up one directory level, then descend into folder1, then descend into folder2
• If the directory path is not valid, then the command will do nothing

touch <filename> - Create a new file

• e.g. touch me.txt creates a file called me.txt in the current directory
• Filenames will only contain characters a-z and "."
• Filenames will contain "." at least once
• "." will never be the first or last character in a file name
• If a file already exists with the same name in the current directory, nothing happens

mkdir <dirname> - Create a new directory

• e.g. mkdir photos creates a new photos directory in the current directory
• Directory names will only contan characters a-z
• If a directory already exists with the same name in the current directory, nothing happens

rm [-r] <filename-or-dirname> - Remove a file or directory

• e.g. rm hello.mp3 removes a file named hello.mp3 in the current directory
• e.g. rm -r documents removes a folder named documents in the current directory, and all of its contents
• If rm tries to delete a directory without the -r flag, nothing will happen
• However rm will delete a file even with the -r flag
• If the specified directory or file cannot be found, nothing happens

Tree output

Your program will output the following tree-like representation of the current file system using spaces as indentation. For example:

/
documents
document.docx
downloads
zippedfile
notavirus.exe
coolgoats.mp3
zippedfile.zip
pictures
myvacation.png

• All directories names must end with a "/" no longer necessary
• You may use any number of spaces to indent the tree (minimum 1)
• Directories must be listed before files in the same directory
• Directories and files should be listed in lexographical alphabetical order
• The character "." lexographically comes before any alphabetical character
• You may output the contents of directories in any order you wish

Challenge

Create a program that accepts a series of commands, and outputs a tree-like representation of the current file system.

Input

The first line of input will be an integer N. Following will be N lines, each containing a command as described above.

You may feel free to omit the number N from your input if it is not necessary

Slight variations are allowed (using commas to seperate commands, input as a list etc) as long as it's reasonable

Output

The contents of the current file system in a tree-like representation, as described above.

Test Cases

Input 1: Simple example from earlier

15
mkdir documents
cd documents
touch document.docx
cd ..
mkdir downloads
cd downloads
touch coolgoats.mp3
touch zippedfile.zip
mkdir zippedfile
cd zippedfile
touch notavirus.exe
cd ../..
mkdir pictures
cd pictures
touch myvacation.png


Output 1:

/
documents
document.docx
downloads
zippedfile
notavirus.exe
coolgoats.mp3
zippedfile.zip
pictures
myvacation.png


Input 2: Incorrect commands and edge cases

12
mkdir folder1
mkdir folder1
mkdir folder2
rm folder1
rm -r folder2
cd ..
cd ../folder1
cd folder1/folder2
touch file.txt
touch file.txt
touch file2.txt
rm -r file2.txt


Output 2:

/
folder1
file.txt


Input 3: Alphabetical listing of directories and files no longer necessary

8
mkdir b
mkdir c
mkdir a
touch c.txt
touch aa.txt
touch b.txt
touch a.txt
touch ab.txt


Output 3:

/
a
b
c
a.txt
aa.txt
ab.txt
b.txt
c.txt


Input 4: Partially correct cd should not be parsed (Suggested by @Arnauld)

4
mkdir folder1
cd folder1
cd ../folder2
touch file.txt


Output 4:

/
folder1
file.txt


Input 5: Partially correct cd should not be parsed (Suggested by @Abigail)

3
mkdir foo
cd bar/../foo
touch file.txt


Output 5:

/
foo
file.txt


Standard loopholes and I/O rules apply.

This is , shortest code wins.

Edit: output requirements have been relaxed a little bit

• "in UTF-8 bytes" — languages that use no more than 256 distinct characters, but have their own character set, are to be punished? – Adám Jul 7 at 16:38
• Starting from an empty state, followed by mkdir foo, does cd bar/../foo descend into foo, or is this an illegal path and thus nothing happens? – Abigail Jul 7 at 17:36
• Explaination for mkdir: "Directory names will only contan characters a-z" – thesilican Jul 7 at 18:18
• @MrSiliconGuy Look here for our default policy on counting bytes. – Adám Jul 7 at 20:11
• @Arnauld that's unecessary, touch description - Filenames will contain "." at least once so names for dirs and files will never overlap – thesilican Jul 8 at 1:33

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. 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 '/'
'

• I think you should be able to remove the spaces ariund && for -2! – Dom Hastings Jul 7 at 18:51
• This is pretty clever, using built-in bash to do all the work for you – thesilican Jul 8 at 1:42
• Can you somehow use chroot here? – the default. Jul 8 at 15:30
• @mypronounismonicareinstate I was just looking at this too, but i needed to alias in the commands... Might be a mechanism there (although needs root) Also, the 3 in mp3 is stripped, so you need to add 0-9 into your sed arg: Try it online! – Dom Hastings Jul 8 at 15:43
• tree --dirsfirst|sed '/,/d;s/[^0-Z.]/ /g;s#^\.#/#' should work with at least GNU sed. Or even sed '$d;s/[^0-Z.]/ /g;s#^\.#/#' if it's just the last line you want to remove, not any lines with a ,. (d is for delete, /,/ is a regular regex condition and $ for the last line) – ilkkachu Jul 8 at 19:34

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+' ')

• Fails for [('mkdir', 'foo'), ('cd', 'foo'), ('touch', 'bar'), ('cd', '..'), ('mkdir', 'foo')]: mkdir should do nothing if the directory already exists, but it always recreates it. – randomdude999 Jul 8 at 12:58
• @randomdude999 thanks for notifying me, I should've read the rules more thoroughly. This is now fixed. – ovs Jul 8 at 13:06
• Alright, now let's get golfing :) The hashes modulo 6 are exactly 3,2,1,0 for "cd","mkdir","rm","touch" respectively, so you can split touch and mkdir, saving 1 byte. You can use a single-line string for the exec and use \n or ; instead of newlines, saving 2 bytes, for a total of 277. try it online! – randomdude999 Jul 8 at 13:16

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

• I'm not sure if it's mandatory, but your answer is missing the leading line with / in that output, that the test cases and other two answers have. – Kevin Cruijssen Jul 8 at 6:49
• @KevinCruijssen I think it previously did not appear in all example outputs, which is why I thought it was not mandatory. Now fixed at no cost anyway. – Arnauld Jul 8 at 7:11
• (@KevinCruijssen Looking at the post history, the / has actually always been there in all examples. I guess I just misread them.) – Arnauld Jul 8 at 7:28
• mkdir foo; cd foo; touch bar.txt; cd ..; mkdir foo (with the semicolons replaced by newlines of course) fails: mkdir should do nothing if the directory already exists, but in this case, it's re-created. – randomdude999 Jul 8 at 13:03
• @randomdude999 Thank you for reporting this. Now fixed. – Arnauld Jul 8 at 13:15