aboutsummaryrefslogtreecommitdiff
#! /usr/bin/env node

"use strict";
let path = require('path');
let fs = require('fs');

let bqn = require("./docs/bqn.js");
module.exports = bqn;
let {fmt,fmtErr,sysvals,sysargs,makebqn,makerepl}=bqn;
let {has,list,str,unstr,dynsys,req1str,makens}=bqn.util;
let strlist = l=>list(l.map(str),str(""));
let bqn_state=makebqn((x,w,u,s)=>(u(s,w),x));
let bqn_nostate=makebqn(x=>x);

let show = x => console.log(fmt(x));
sysvals.show = (x,w) => { show(x); return x; };
sysvals.out = (x,w) => { console.log(req1str("•Out",x,w)); return x; };
sysvals.exit = (x,w) => process.exit(Number.isInteger(x)?x:0);

let dir = f=>f==='/'?f:f+'/'; // BQN uses trailing slash
let getres = p => {
  let res;
  if (p) { p=unstr(p); res = (e,f)=>path.resolve(p,f); }
  else { res = (e,f) => { if (!path.isAbsolute(f)) throw Error(e+": Paths must be absolute when not running from a file"); return f; }; }
  return e => (x,w) => res(e,req1str(e,x,w));
}
let withres = (e,fn) => dynsys(state => fn(state.resolve(e), state));
let ff = (fr,fw,o) => resolve => (x,w) => {
  let f = resolve(has(w)?w:x);
  if (has(w)) { fs.writeFileSync(f,fw(x),o); return str(f); }
  else { return fr(fs.readFileSync(f,o)); }
};
let fchars = ff(str,unstr,"utf-8");
let flines = ff(s=>strlist(s.replace(/\n$/,'').split('\n')),s=>s.map(unstr).join('\n')+'\n',"utf-8");
let fbytes = ff(s=>list(Array.from(s).map(c=>String.fromCodePoint(c))),s=>Buffer.from(s.map(c=>c.codePointAt(0))));
sysvals.fchars = withres("•FChars",fchars);
sysvals.flines = withres("•FLines",flines);
sysvals.fbytes = withres("•FBytes",fbytes);

sysvals.file = dynsys(state => {
  let p = state.path;
  let res = state.resolve;
  let files = {
    // Paths and parsing
    path: p,
    at: (x,w) => {
      let e="•file.At";
      return str(has(w)?path.join(req1str(e,w),req1str(e,x)):res(e)(x));
    },
    name:      (x,w) => str(path.basename(req1str("•file.Name",x,w))),
    extension: (x,w) => str(path.extname (req1str("•file.Extension",x,w))),
    basename:  (x,w) => str(path.parse(req1str("•file.BaseName",x,w)).name),
    parent:    (x,w) => str(dir(path.dirname(res("•file.Parent")(x,w)))),
    parts:     (x,w) => { let p=path.parse(res("•file.Parts")(x,w));
                          return strlist([dir(p.dir),p.name,p.ext]); },

    // Metadata
    exists: (x,w) => fs.existsSync(res("•file.Exists")(x,w))?1:0,
    type: (x,w) => {
      let c = fs.constants;
      switch (c.S_IFMT & fs.lstatSync(res("•file.Type")(x,w)).mode) {
        case c.S_IFREG:  return 'f';
        case c.S_IFDIR:  return 'd';
        case c.S_IFLNK:  return 'l';
        case c.S_IFIFO:  return 'p';
        case c.S_IFSOCK: return 's';
        case c.S_IFBLK:  return 'b';
        case c.S_IFCHR:  return 'c';
      }
    },
    created:  (x,w) => fs.statSync(res("•file.Created" )(x,w)).birthtimeMs/1000,
    accessed: (x,w) => fs.statSync(res("•file.Accessed")(x,w)).atimeMs/1000,
    modified: (x,w) => fs.statSync(res("•file.Modified")(x,w)).mtimeMs/1000,
    size:     (x,w) => fs.statSync(res("•file.Size"    )(x,w)).size,
    permissions: (x,w) => {
      let f=res("•file.Permissions")(x);
      let mode=fs.statSync(f).mode;
      if (has(w)) {
        if (!w.sh||w.sh.length!==1||w.sh[0]!==3) throw Error("•file.Permissions: 𝕨 must be a list of 3 numbers");
        if (!w.every(n=>Number.isInteger(n)&&0<=n&&n<8)) throw Error("•file.Permissions: each permission must belong to ↕8");
        let p=0; w.map(n=>p=8*p+n);
        fs.chmodSync(f,(mode&fs.constants.S_IFMT)|p); return w;
      } else {
        let p=[]; for (let i=3;i--;) { p[i]=mode&7; mode=Math.floor(mode/8); }
        return list(p,0);
      }
    },
    owner: (x,w) => {
      let f=res("•file.Owner")(x);
      if (has(w)) {
        if (!w.sh||w.sh.length!==1||w.sh[0]!==2) throw Error("•file.Owner: 𝕨 must be a uid‿gid pair");
        fs.chownSync(f,w[0],w[1]); return w;
      } else {
        let s=fs.statSync(f); return list([s.uid,s.gid],0);
      }
    },

    // Access
    rename:    (x,w) => {let r=res("•file.Rename"),f=r(w); fs.renameSync  (r(x),f); return str(f);},
    copy:      (x,w) => {let r=res("•file.Copy"  ),f=r(w); fs.copyFileSync(r(x),f); return str(f);},
    createdir: (x,w) => {let f=res("•file.CreateDir")(x,w); fs.mkdirSync(f); return str(f);},
    remove:    (x,w) => {fs.rmSync(res("•file.Remove")(x,w)); return 1;},
    removedir: (x,w) => {fs.rmSync(res("•file.RemoveDir")(x,w),{recursive:true,force:true}); return 1;},
    list:  (x,w) => strlist(fs.readdirSync(res("•file.List")(x,w))),
    chars: fchars(res("•file.Chars")),
    lines: flines(res("•file.Lines")),
    bytes: fbytes(res("•file.Bytes")),
    // TODO Open Return an open file object based on 𝕩
  }
  return makens(Object.keys(files), Object.values(files));
});

sysvals.getline = () => {
  let l = 1024, b = Buffer.alloc(l);
  let fd = fs.openSync("/dev/stdin", "rs");
  let r = '';
  do {
    let n = fs.readSync(fd, b, 0, b.length);
    if (!n) return '\0';
    r += b.toString('utf-8', 0, n);
  } while (r[r.length-1]!=='\n');
  fs.closeSync(fd);
  return str(r.slice(0,-1));
}

sysargs.resolve = sysargs.parres = getres();
let push_state = st => { st.parres = st.resolve; }
let update_state = (st,w) => {
  w=w||[];
  st.path=w[0]&&str(st.parres("Setting •path")(w[0]));
  st.resolve = getres(st.path);
  st.state=list(w); st.name=w[1]; st.args=w[2];
}
sysvals.path=dynsys(s=>s.path);
sysvals.name=dynsys(s=>s.name);
sysvals.args=dynsys(s=>s.args);
sysvals.state=dynsys(s=>s.state);
sysvals.wdpath=dynsys(_=>str(dir(path.resolve('.'))));
bqn.setexec(update_state, push_state);
let bqn_file = (st,f,t,w) => bqn_state(st)(
  t, [ str(dir(path.dirname(f))), str(path.basename(f)), w ]
);
let imports = {};
sysvals.import = withres("•Import", (resolve,state) => (x,w) => {
  let f = resolve(x);
  let save = r=>r;
  if (!has(w)) {
    let c=imports[f]; if (has(c)) return c;
    save = r => (imports[f]=r);
    w=list([]);
  }
  return save(bqn_file(state, f, fs.readFileSync(f,'utf-8'), w));
});

if (!module.parent) {
  let args = process.argv.slice(2);
  let arg0 = args[0];
  let cl_state = () => {
    let s = str("");
    let w = [str(dir(path.resolve('.'))), s, list([],s)];
    update_state(sysargs, w); return sysargs;
  }
  let exec = fn => src => {
    try {
      fn(src);
    } catch(e) {
      console.error(''+fmtErr(e)+'');
    }
  }
  if (!has(arg0) || arg0==='-r') {
    let stdin = process.stdin;
    let repl = makerepl(cl_state(), 1);
    let e = exec(s=>show(repl(s)));
    stdin.on('end', () => { process.exit(); });
    stdin.on('readable', () => {
      let inp; while ((inp=stdin.read())!==null) {
        if (!/^[ \t]*[#\n]/.test(inp)) e(inp.toString());
      }
    });
  } else if (arg0[0] !== '-' || (arg0==='-f'&&(arg0=(args=args.slice(1))[0],1))) {
    let f=arg0, a=strlist(args.slice(1));
    exec(s=>bqn_file(sysargs, path.resolve(f),s,a))(fs.readFileSync(f,'utf-8'));
  } else if (arg0 === '-e' || arg0 === '-p') {
    let ev=bqn_nostate(cl_state());
    let evs = arg0!=='-p' ? ev : (s=>show(ev(s)));
    args.slice(1).map(exec(evs));
  }
}