13
\$\begingroup\$

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

\$\endgroup\$
16
  • 3
    \$\begingroup\$ "in UTF-8 bytes" — languages that use no more than 256 distinct characters, but have their own character set, are to be punished? \$\endgroup\$
    – Adám
    Jul 7, 2020 at 16:38
  • 1
    \$\begingroup\$ 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? \$\endgroup\$
    – Abigail
    Jul 7, 2020 at 17:36
  • 1
    \$\begingroup\$ Explaination for mkdir: "Directory names will only contan characters a-z" \$\endgroup\$
    – thesilican
    Jul 7, 2020 at 18:18
  • 2
    \$\begingroup\$ @MrSiliconGuy Look here for our default policy on counting bytes. \$\endgroup\$
    – Adám
    Jul 7, 2020 at 20:11
  • 1
    \$\begingroup\$ @Arnauld that's unecessary, touch description - Filenames will contain "." at least once so names for dirs and files will never overlap \$\endgroup\$
    – thesilican
    Jul 8, 2020 at 1:33

3 Answers 3

9
\$\begingroup\$

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+' ')
\$\endgroup\$
3
  • 1
    \$\begingroup\$ 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. \$\endgroup\$ Jul 8, 2020 at 12:58
  • \$\begingroup\$ @randomdude999 thanks for notifying me, I should've read the rules more thoroughly. This is now fixed. \$\endgroup\$
    – ovs
    Jul 8, 2020 at 13:06
  • 1
    \$\begingroup\$ 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! \$\endgroup\$ Jul 8, 2020 at 13:16
7
\$\begingroup\$

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 '/'
 '
\$\endgroup\$
10
  • \$\begingroup\$ I think you should be able to remove the spaces ariund` &&` for -2! \$\endgroup\$ Jul 7, 2020 at 18:51
  • \$\begingroup\$ This is pretty clever, using built-in bash to do all the work for you \$\endgroup\$
    – thesilican
    Jul 8, 2020 at 1:42
  • 1
    \$\begingroup\$ Can you somehow use chroot here? \$\endgroup\$ Jul 8, 2020 at 15:30
  • \$\begingroup\$ @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! \$\endgroup\$ Jul 8, 2020 at 15:43
  • 1
    \$\begingroup\$ 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) \$\endgroup\$
    – ilkkachu
    Jul 8, 2020 at 19:34
7
\$\begingroup\$

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
\$\endgroup\$
5
  • \$\begingroup\$ 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. \$\endgroup\$ Jul 8, 2020 at 6:49
  • \$\begingroup\$ @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. \$\endgroup\$
    – Arnauld
    Jul 8, 2020 at 7:11
  • \$\begingroup\$ (@KevinCruijssen Looking at the post history, the / has actually always been there in all examples. I guess I just misread them.) \$\endgroup\$
    – Arnauld
    Jul 8, 2020 at 7:28
  • \$\begingroup\$ 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. \$\endgroup\$ Jul 8, 2020 at 13:03
  • \$\begingroup\$ @randomdude999 Thank you for reporting this. Now fixed. \$\endgroup\$
    – Arnauld
    Jul 8, 2020 at 13:15

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.