-
Notifications
You must be signed in to change notification settings - Fork 12
/
Builtins.fs
236 lines (217 loc) · 11.3 KB
/
Builtins.fs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
/// All builtins are defined and aggregated here.
/// A builtin is a custom command that provides a shell function, e.g. cd which changes the shells current directory.
/// The builtins are each a custom function that takes input arguments.
/// The final 'builtin' list exposes these functions and the command that invokes them (usually the same, but there are synonyms like ? for help)
/// to the processCommand function in Program.fs
module Builtins
open System
open System.IO
open System.Collections
open Common
/// Returns the current process directory. By default this is where it was started, and can be changed with the cd builtin.
let currentDir () = Directory.GetCurrentDirectory ()
/// Clears the console window.
let private clear _ _ _ =
Console.Clear ()
/// Reads out the arguments passed into the output.
let private echo args writeOut _ =
writeOut (sprintf "%s" (String.concat " " args))
/// Lists the contents of the current directory.
let private dir args writeOut _ =
let searchPath = Path.Combine(currentDir (), if List.isEmpty args then "" else args.[0])
let searchPattern = if List.length args < 2 then "*" else args.[1]
if File.Exists searchPath then
writeOut (sprintf "%s" (Path.GetFileName searchPath))
else
let finalPath, finalPattern =
if Directory.Exists searchPath then searchPath, searchPattern
else if searchPattern = "*" then Path.GetDirectoryName searchPath, Path.GetFileName searchPath
else Path.GetDirectoryName searchPath, searchPattern
Directory.GetDirectories (finalPath, finalPattern)
|> Seq.map (Path.GetFileName >> sprintf "%s/")
|> Seq.iter writeOut
Directory.GetFiles (finalPath, finalPattern)
|> Seq.map (Path.GetFileName >> sprintf "%s")
|> Seq.iter writeOut
/// Changes the curent process directory.
let private cd args _ writeError =
if List.isEmpty args then writeError "no path specified"
elif args.[0] = "\\" && isWindows then Directory.SetCurrentDirectory "/"
elif args.[0] = "~" then
Directory.SetCurrentDirectory(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile))
else
let newPath = Path.Combine (currentDir (), args.[0])
let newPath = if newPath.EndsWith "/" then newPath else newPath + "/"
if Directory.Exists newPath then
Directory.SetCurrentDirectory newPath
else
writeError "directory not found"
/// Creates a new empty directory.
let private mkdir args writeOut writeError =
if List.isEmpty args then
writeError "no directory name speciifed"
else
let path = Path.Combine (currentDir(), args.[0])
if Directory.Exists path then
writeError "directory already exists"
else
Directory.CreateDirectory path |> ignore
writeOut "directory created"
/// Deletes a directory, if empty.
let private rmdir args writeOut writeError =
if List.isEmpty args then
writeError "no directory name speciifed"
else
let isForced = args.[0] = "-f"
let path = Path.Combine (currentDir(), args.[if isForced then 1 else 0])
if not (Directory.Exists path) then
writeError "directory does not exist"
elif not isForced && Directory.GetFiles (path, "*", SearchOption.AllDirectories) |> Array.isEmpty |> not then
writeError "directory was not empty"
else
Directory.Delete (path, isForced) |> ignore
writeOut "directory deleted"
/// Reads out the content of a file to the output.
let private cat args writeOut writeError =
if List.isEmpty args then
writeError "no file specified"
elif not (File.Exists args.[0]) then
writeError "file not found"
else
writeOut (File.ReadAllText args.[0])
/// Copies a file into a new location.
let private cp args writeOut writeError =
if List.length args < 2 then
writeError "wrong number of arguments: please specify source and dest"
else
let isForced = args.[0] = "-f"
let source = Path.Combine(currentDir(), args.[if isForced then 1 else 0])
if not (File.Exists source) then
writeError "source file path does not exist or is invalid"
else
let dest = Path.Combine(currentDir(), args.[if isForced then 2 else 1])
let isDir = Directory.Exists dest
let baseDir = Path.GetDirectoryName dest
if not isDir && not (Directory.Exists baseDir) then
writeError "destination directory or file path does not exist or is invalid"
elif not isForced && not isDir && File.Exists dest then
writeError "destination file already exists"
elif not isDir then
if File.Exists dest then File.Delete dest
File.Copy (source, dest)
writeOut "file copied"
else
let fileName = Path.GetFileName source
let dest = Path.Combine(dest, fileName)
if not isForced && File.Exists dest then
writeError "destination file already exists"
else
if File.Exists dest then File.Delete dest
File.Copy (source, dest)
writeOut "file copied"
/// Moves a file to a new location.
let private mv args writeOut writeError =
if List.length args < 2 then
writeError "wrong number of arguments: please specify source and dest"
else
let isForced = args.[0] = "-f"
let source = Path.Combine(currentDir(), args.[if isForced then 1 else 0])
if not (File.Exists source) then
writeError "source file path does not exist or is invalid"
else
let dest = Path.Combine(currentDir(), args.[if isForced then 2 else 1])
let isDir = Directory.Exists dest
let baseDir = Path.GetDirectoryName dest
if not isDir && not (Directory.Exists baseDir) then
writeError "destination directory or file path does not exist or is invalid"
elif not isForced && not isDir && File.Exists dest then
writeError "destination file already exists"
elif not isDir then
if File.Exists dest then File.Delete dest
File.Move (source, dest)
writeOut "file moved"
else
let fileName = Path.GetFileName source
let dest = Path.Combine(dest, fileName)
if not isForced && File.Exists dest then
writeError "destination file already exists"
else
if File.Exists dest then File.Delete dest
File.Move (source, dest)
writeOut "file moved"
/// Removes a file or directory.
let private rm args writeOut writeError =
if List.length args < 1 then
writeError "no target specified"
else
let isForced = args.[0] = "-f"
let target = Path.Combine(currentDir(), args.[if isForced then 1 else 0])
if File.Exists target then
File.Delete target
writeOut "file deleted"
else if Directory.Exists target then
if not isForced && not (Array.isEmpty (Directory.GetFiles target)) then
writeError "directory is not empty"
else
Directory.Delete (target, isForced)
writeOut "directory deleted"
else
writeError "file or directory does not exist"
/// Enumerates all environment variables, or reads/sets a single value.
let private env args writeOut _ =
if List.isEmpty args then
Environment.GetEnvironmentVariables ()
|> Seq.cast<DictionaryEntry>
|> Seq.map (fun d -> sprintf "%s=%s" (unbox<string> d.Key) (unbox<string> d.Value))
|> Seq.iter writeOut
else if args.Length = 1 then
writeOut (Environment.GetEnvironmentVariable args.[0])
else
Environment.SetEnvironmentVariable (args.[0], args.[1])
/// This list maps the text entered by the user to the implementation to be run, and the help text for the command.
let builtins =
[
"clear", (clear, "clears the console")
"echo", (echo, "prints out all text following the echo command to output")
"dir", (dir, "same as ls, will list all files and directories. arguments are [path] [searchPattern], both optional")
"ls", (dir, "same as dir, will list all files and directories. arguments are [path] [searchPattern], both optional")
"cd", (cd, "changes the current directory to the directory specified by the first argument")
"mkdir", (mkdir, "creates a new directory at the position specified by the first argument")
"rmdir", (rmdir, "removes a directory (use 'rmdir -f [dir]' to remove directories containing files)")
"cat", (cat, "prints the contents of the file specified to the output")
"cp", (cp, "copies the source file to the destination folder or filepath (use cp -f [src] [dest] to overwrite)")
"mv", (mv, "moves the source file to the destination folder or filepath (use mv -f [src] [dest] to overwrite)")
"rm", (rm, "same as del, deletes the target file or directory (use rm -f [dir] to delete a directory containing files)")
"del", (rm, "same as rm, deletes the target file or directory (use del -f [dir] to delete a directory containing files)")
"env", (env, "either lists all env vars, reads a specific var, or sets a var to a value")
// The following three special builtins are here so that help can access their content.
// However they have no implementation, as they are invoked by the coreloop and processCommand methods
// in Program.fs rather than via the normal builtin execution process.
"?", ((fun _ _ _ -> ()), "same as help, prints the builtin list, or the help of specific commands")
"help", ((fun _ _ _ -> ()), "same as ?, prints the builtin list, or the help of specific commands")
"exit", ((fun _ _ _ -> ()), "exits FSH")
]
/// The map is specifically used to match entered text to implementation.
let builtinMap =
builtins
|> List.map (fun (commandName, (implementation, _)) -> commandName, implementation)
|> Map.ofList
// For the above code, the reason why the map and list is seperate is that a map is ordered by its keys.
// For help, the order of builtin's is important and is preserved in the list.
/// Help provides helptext for a given builtin (including itself).
/// It is defined after the builtin map, as it needs to read from the map to perform its function.
let help args writeOut _ =
[
if List.isEmpty args then
yield sprintf "\nThe following builtin commands are supported by FSH:\n"
yield! builtins |> List.map (fun (n, _) -> sprintf "\t%s" n)
yield sprintf "\nFor further info on a command, use help [command name] [command name2] etc, e.g. 'help echo'\n"
else
yield!
args
|> List.choose (fun commandName ->
List.tryFind (fun item -> fst item = commandName) builtins
|> Option.bind (fun (_, (_, helpText)) -> Some (commandName, helpText)))
|> List.map (fun result ->
result ||> sprintf "%s: %s")
] |> Seq.iter writeOut