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 sed
s).
-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