403 lines
11 KiB
TypeScript
Executable File
403 lines
11 KiB
TypeScript
Executable File
import { Bash, ExitCode, type Permission, type TimeStamps } from '../bash';
|
|
import { Type, type NodePerms, type TreeNode } from '../fs';
|
|
import { Sort, SortNodeBy } from '../sort';
|
|
import type { CommandArgs, ICommand, Result, resultData } from '../static';
|
|
|
|
type LsEntry = {
|
|
inode: number | null;
|
|
perms: string;
|
|
children: string;
|
|
owners: string;
|
|
size: string;
|
|
modt: string;
|
|
name: string;
|
|
};
|
|
|
|
const LsEntryUtils = {
|
|
toString(entry: LsEntry): string {
|
|
return Object.entries(entry)
|
|
.filter(([_, value]) => value !== '')
|
|
.map(([_, value]) => value)
|
|
.join(' ');
|
|
}
|
|
};
|
|
|
|
const months: readonly string[] = [
|
|
'Jan',
|
|
'Feb',
|
|
'Mar',
|
|
'Apr',
|
|
'May',
|
|
'Jun',
|
|
'Jul',
|
|
'Aug',
|
|
'Sep',
|
|
'Oct',
|
|
'Nov',
|
|
'Dec'
|
|
];
|
|
|
|
export const cmd_ls = function (this: Bash, args: CommandArgs): Result {
|
|
const resultData: resultData = { cmd: 'ls', data: null, args: args };
|
|
const result: Result = { exitCode: ExitCode.ERROR, path: this.getCwd(), data: resultData };
|
|
const nodes: TreeNode[] = [];
|
|
|
|
//Check if any args contain the long flags with value and are valid flags inside the ls const
|
|
const valuedArgs = args.flags.filter((flag: string) =>
|
|
flag.includes('=') && ls.flags.includes(flag.split('=')[0]));
|
|
console.log(valuedArgs);
|
|
|
|
//Check if args contain any nonexistent flags, if so add it to an array and check its length. if 0 no bad flags
|
|
const invalidArgs = args.flags.filter((flag) => !ls.flags.includes(flag) && !valuedArgs.includes(flag));
|
|
console.log(invalidArgs);
|
|
|
|
if (invalidArgs.length > 0) {
|
|
this.throwError(result); //No such flag/s
|
|
}
|
|
|
|
if (args.args.length === 0) nodes.push(this.getFs().getNodeByINode(this.getFs().cwd));
|
|
|
|
for (let i = 0; i < args.args.length; i++) {
|
|
const node = this.getFs().resolvePath(args.args[i]);
|
|
if (node === null) this.throwError(result); //no such path (i think this will never occur as backed methods have error cases implemented - which is wrong)
|
|
|
|
nodes.push(node);
|
|
}
|
|
|
|
result.exitCode = ExitCode.SUCCESS;
|
|
resultData.data = result_ls.call(this, nodes, args);
|
|
return result;
|
|
};
|
|
|
|
function result_ls(this: Bash, data: any, args: CommandArgs): HTMLElement {
|
|
const dummysonoerror: HTMLElement = document.createElement('div');
|
|
|
|
const flagInfo = checkFlags(args.flags);
|
|
const nodes: TreeNode[] = data;
|
|
|
|
const f_a: boolean = flagInfo.has('a') || flagInfo.has('all');
|
|
const f_A: boolean = flagInfo.has('A') || flagInfo.has('almost-all');
|
|
const f_G: boolean = flagInfo.has('G') || flagInfo.has('no-group');
|
|
const f_h: boolean = flagInfo.has('h') || flagInfo.has('human-readable');
|
|
const f_r: boolean = flagInfo.has('r') || flagInfo.has('reverse');
|
|
const f_Q: boolean = flagInfo.has('Q') || flagInfo.has('quote-name');
|
|
const f_n: boolean = flagInfo.has('n') || flagInfo.has('numeric-uid-gid');
|
|
const f_N: boolean = flagInfo.has('N') || flagInfo.has('literal');
|
|
const f_L: boolean = flagInfo.has('L') || flagInfo.has('dereference');
|
|
const f_i: boolean = flagInfo.has('i') || flagInfo.has('inode');
|
|
const f_help: boolean = flagInfo.has('help');
|
|
const f_si: boolean = flagInfo.has('si');
|
|
const f_X: boolean = flagInfo.has('X');
|
|
const f_S: boolean = flagInfo.has('S');
|
|
const f_t: boolean = flagInfo.has('t');
|
|
const f_l: boolean = flagInfo.has('l');
|
|
const f_U: boolean = flagInfo.has('U');
|
|
const f_f: boolean = flagInfo.has('f');
|
|
const f_g: boolean = flagInfo.has('g');
|
|
const f_o: boolean = flagInfo.has('o');
|
|
|
|
let shouldShift: boolean = false
|
|
|
|
const valuedArgs = args.flags.filter((flag: string) =>
|
|
flag.includes('=') && ls.flags.includes(flag.split('=')[0]));
|
|
|
|
if (f_l || f_g || f_o) {
|
|
const w: HTMLElement = document.createElement('div');
|
|
|
|
for (const node of nodes) {
|
|
const elem: HTMLElement = document.createElement('div');
|
|
const children: TreeNode[] = node.children.map((child) => this.getFs().getNodeByINode(child));
|
|
const rows: string[] = [];
|
|
|
|
const timeArg = valuedArgs.find((flag) => flag.startsWith('time'));
|
|
let timestamp: SortNodeBy.ATIME | SortNodeBy.CTIME | SortNodeBy.MTIME = SortNodeBy.MTIME;
|
|
if(timeArg) {
|
|
let value: string = timeArg.split('=')[1];
|
|
if (value && isValidNodeTimestamp(value)) {
|
|
timestamp = value;
|
|
console.log(timestamp);
|
|
}
|
|
}
|
|
|
|
if (!f_U && !f_f) {
|
|
const sortArg = valuedArgs.find((flag) => flag.startsWith('sort'));
|
|
let sortBy: SortNodeBy = SortNodeBy.NAME;
|
|
if(f_t) sortBy = timestamp;
|
|
if(f_S) sortBy = SortNodeBy.SIZE;
|
|
if(f_X) sortBy = SortNodeBy.EXTENSION;
|
|
if(sortArg) {
|
|
let value = sortArg.split('=')[1];
|
|
if(value && isValidNodeSortMethod(value)) {
|
|
sortBy = value;
|
|
console.log(sortBy, 'sortBy');
|
|
}
|
|
}
|
|
|
|
Sort.nodeArraySort.call(this, children, f_r, sortBy);
|
|
}
|
|
|
|
const sizes = children.map((child) => child.size);
|
|
const maxSizeWidth = Math.max(...sizes.map((size) => size));
|
|
|
|
for (const child of children) {
|
|
if (child.name.startsWith('.') && !(f_a || f_A)) continue;
|
|
|
|
const cols: LsEntry = {
|
|
inode: null,
|
|
perms: formatPermission(child),
|
|
children: formatChildren(child),
|
|
owners: formatOwners.call(this, child, flagInfo),
|
|
size: formatSize.call(this, f_h, child, maxSizeWidth, f_si),
|
|
modt: formatModtime(child, timestamp),
|
|
name: formatName(child, flagInfo, shouldShift)
|
|
};
|
|
|
|
if (f_i) cols.inode = child.inode;
|
|
|
|
rows.push(LsEntryUtils.toString(cols));
|
|
}
|
|
|
|
if (f_a && !f_A) {
|
|
const current: LsEntry = {
|
|
inode: null,
|
|
perms: formatPermission(node),
|
|
children: formatChildren(node),
|
|
owners: formatOwners.call(this, node, flagInfo),
|
|
size: formatSize.call(this, f_h, node, maxSizeWidth, f_si),
|
|
modt: formatModtime(node, timestamp),
|
|
name: '.'
|
|
};
|
|
let parent: LsEntry = {
|
|
...current,
|
|
name: '..'
|
|
};
|
|
if (node.parent) {
|
|
const parentNode: TreeNode = this.getFs().getNodeByINode(node.parent);
|
|
parent = {
|
|
inode: null,
|
|
perms: formatPermission(parentNode),
|
|
children: formatChildren(parentNode),
|
|
owners: formatOwners.call(this, parentNode, flagInfo),
|
|
size: formatSize.call(this, f_h, parentNode, maxSizeWidth, f_si),
|
|
modt: formatModtime(parentNode, timestamp),
|
|
name: '..'
|
|
};
|
|
}
|
|
|
|
if (f_i) {
|
|
current.inode = node.inode;
|
|
parent.inode = node.parent ? node.parent : node.inode;
|
|
}
|
|
|
|
if (f_r) {
|
|
rows.push(LsEntryUtils.toString(parent), LsEntryUtils.toString(current));
|
|
} else {
|
|
rows.unshift(LsEntryUtils.toString(current), LsEntryUtils.toString(parent));
|
|
}
|
|
}
|
|
|
|
//TODO: Calculate the total size of contents in the node
|
|
rows.unshift('total ' + node.children.length.toString());
|
|
|
|
if (nodes.length > 1) {
|
|
const nodePath: string =
|
|
node.name === '/' ? '/:' : `${this.getFs().getPathByInode(node.inode).slice(1)}:`;
|
|
rows.unshift(nodePath);
|
|
rows.push('\n');
|
|
}
|
|
|
|
for(const row of rows) {
|
|
const name: string = row[row.length - 1];
|
|
if(!name.startsWith('"') || !name.startsWith("'"))
|
|
name.padStart(1, ' ');
|
|
else continue;
|
|
}
|
|
|
|
for (let i = 0; i < rows.length; i++) {
|
|
const p: HTMLElement = document.createElement('p');
|
|
|
|
|
|
p.innerText = rows[i];
|
|
|
|
elem.appendChild(p);
|
|
}
|
|
|
|
w.appendChild(elem);
|
|
}
|
|
return w;
|
|
}
|
|
|
|
|
|
|
|
return dummysonoerror; //TEMP SO NO ERROR CUZ RETURNS HTMLElement EVERY TIME, DELETE LATER
|
|
}
|
|
|
|
function checkMaxColumns(this: Bash, files: TreeNode[]) {
|
|
const paddingWidth: number = 2;
|
|
const maxWidth: number = (this.getTerminalWidth() / this.getTerminalFontSize()) * 0.8;
|
|
|
|
|
|
//Upper bound set to max width / min column width - 1 character + padding
|
|
let lowBound: number = 1;
|
|
let highBound: number = Math.floor(maxWidth / (1 + paddingWidth));
|
|
|
|
|
|
|
|
}
|
|
|
|
function isValidNodeSortMethod(value: string): value is SortNodeBy {
|
|
return Object.values(SortNodeBy).includes(value as SortNodeBy);
|
|
}
|
|
|
|
function isValidNodeTimestamp(value: string): value is SortNodeBy.ATIME | SortNodeBy.CTIME | SortNodeBy.MTIME {
|
|
return Object.values(SortNodeBy).includes(value as SortNodeBy.ATIME | SortNodeBy.CTIME | SortNodeBy.MTIME);
|
|
}
|
|
|
|
function parsePerms(perms: NodePerms): string {
|
|
const parts: string[] = [];
|
|
//for each key (key representing key name and p representing the key contents) of entries in perms as types keyof NodePerms and Permission
|
|
for (const [key, p] of Object.entries(perms) as [keyof NodePerms, Permission][]) {
|
|
const perms: string = `${p.r ? 'r' : '-'}${p.w ? 'w' : '-'}${p.x ? 'x' : '-'}`;
|
|
parts.push(perms);
|
|
}
|
|
return parts.join('');
|
|
}
|
|
|
|
function formatOwners(this: Bash, node: TreeNode, flag: any): string {
|
|
const owner: string = this.getUserByUid(node.owner).username;
|
|
const group: string = this.getGroupByGid(node.group).groupname;
|
|
|
|
if (flag.has('G') || flag.has('o')) {
|
|
if (flag.has('n')) return `${node.owner}`;
|
|
return `${owner}`;
|
|
}
|
|
|
|
if (flag.has('g')) {
|
|
if (flag.has('n')) return `${node.group}`;
|
|
return `${group}`;
|
|
}
|
|
|
|
if (flag.has('n')) return `${node.owner} ${node.group}`;
|
|
|
|
return `${owner} ${group}`;
|
|
}
|
|
|
|
function formatPermission(node: TreeNode): string {
|
|
switch(node.type) {
|
|
case Type.Directory:
|
|
return `d${parsePerms(node.permission)}`;
|
|
case Type.File:
|
|
return `-${parsePerms(node.permission)}`;
|
|
case Type.SymbolicLink:
|
|
return `l${parsePerms(node.permission)}`;
|
|
default:
|
|
throw new Error(`Node of unexpected type - ${node.type}`);
|
|
}
|
|
}
|
|
|
|
function formatChildren(node: TreeNode): string {
|
|
if (node.type !== Type.Directory) return ' 0';
|
|
if (!node.children) throw new Error('children array is null on this node');
|
|
|
|
const c = node.children.length.toString();
|
|
return c.length > 1 ? c : ` ${c}`;
|
|
}
|
|
|
|
function formatSize(
|
|
this: Bash,
|
|
humanReadable: boolean,
|
|
node: TreeNode,
|
|
max: number,
|
|
f_si: boolean
|
|
): string {
|
|
let size: string;
|
|
if (humanReadable) {
|
|
size = this.formatBytes(node.size, 1, f_si ? 1000 : 1024);
|
|
} else size = node.size.toString();
|
|
|
|
return size.padStart(max, ' ');
|
|
}
|
|
|
|
function formatModtime(node: TreeNode, sortBy: SortNodeBy.ATIME | SortNodeBy.CTIME | SortNodeBy.MTIME): string {
|
|
const now = new Date();
|
|
//TODO: Change this to be dynamic based on the --time value passed
|
|
const hours: string = node.timestamps[sortBy].getHours().toString().padStart(2, '0');
|
|
const minutes: string = node.timestamps[sortBy].getMinutes().toString().padStart(2, '0');
|
|
const time: string =
|
|
now.getFullYear() === node.timestamps[sortBy].getFullYear()
|
|
? `${hours}:${minutes}`
|
|
: node.timestamps[sortBy].getFullYear().toString();
|
|
|
|
return [
|
|
months[node.timestamps[sortBy].getMonth()],
|
|
node.timestamps[sortBy].getDate().toString().padStart(2, ' '),
|
|
`${time}`
|
|
].join(' ');
|
|
}
|
|
|
|
function formatName(node: TreeNode, flag: any, shouldShift: boolean) {
|
|
let name: string;
|
|
const char: string = flag.has('Q') ? '"' : "'";
|
|
|
|
if (/\s/.test(node.name)) {
|
|
name = `${char}${node.name}${char}`
|
|
shouldShift = true;
|
|
} else {
|
|
name = `${node.name}`;
|
|
}
|
|
|
|
return flag.has('p') && node.type === Type.Directory ? `${name}/` : name;
|
|
}
|
|
|
|
const checkFlags = (pFlags: string[]) => {
|
|
const flagSet = new Set(pFlags);
|
|
|
|
return { has: (flag: string) => flagSet.has(flag) };
|
|
};
|
|
|
|
export const ls: ICommand = {
|
|
method: cmd_ls,
|
|
flags: [
|
|
'l',
|
|
'a',
|
|
'A',
|
|
'c',
|
|
'U',
|
|
'g',
|
|
'G',
|
|
'h',
|
|
'f',
|
|
'x',
|
|
'X',
|
|
'u',
|
|
't',
|
|
'S',
|
|
'r',
|
|
'Q',
|
|
'p',
|
|
'o',
|
|
'n',
|
|
'N',
|
|
'L',
|
|
'm',
|
|
'i',
|
|
'sort',
|
|
'time',
|
|
'help',
|
|
'all',
|
|
'almost-all',
|
|
'no-group',
|
|
'human-readable',
|
|
'reverse',
|
|
'quote-name',
|
|
'indicator-style',
|
|
'literal',
|
|
'numeric-uid-gid',
|
|
'inode',
|
|
'si',
|
|
'dereference'
|
|
] as string[],
|
|
help: 'PATH TO HELP.MD',
|
|
root: false
|
|
};
|