diff --git a/shell/file/file.go b/shell/file/file.go new file mode 100644 index 0000000..2d582be --- /dev/null +++ b/shell/file/file.go @@ -0,0 +1,333 @@ +package file + +import ( + "os" + "os/user" + "path/filepath" + "strconv" + "strings" + "sync" + "syscall" + "time" + + . "gitea.zaclys.com/bvaudour/gob/option" +) + +var ( + owners = make(map[string]string) + groups = make(map[string]string) +) + +// File représente la description d’un fichier sur un disque. +type File struct { + info os.FileInfo + dirname string + name string + parent Option[*File] + children FileList +} + +// New récupère les informations d’un fichier à partir de son nom. +// et de son répertoire d’appartenance. +func New(dirname, name string) (f Result[*File]) { + info, err := os.Lstat(filepath.Join(dirname, name)) + + if err == nil { + return Ok(&File{ + info: info, + dirname: dirname, + name: name, + }) + } + return Err[*File](err) +} + +// NewFromInfo agit comme New mais en ayant déjà récupéré une +// partie de la description. +func NewFromInfo(dirname string, info os.FileInfo) *File { + return &File{ + info: info, + dirname: dirname, + name: info.Name(), + } +} + +func (f *File) stat() *syscall.Stat_t { return f.info.Sys().(*syscall.Stat_t) } + +// Mod retourne les droits UNIX du fichier. +func (f *File) Mode() os.FileMode { return f.info.Mode() } + +// HasMode vérifie que le fichier a un des droits spécifiés. +func (f *File) HasMode(mask os.FileMode) bool { return f.Mode()&mask != 0 } + +// IsDir retourne vrai si le fichier est un répertoire. +func (f *File) IsDir() bool { return f.info.IsDir() } + +// IsSymlink retourne vrai si le fichier est un lien symbolique. +func (f *File) IsSymlink() bool { return f.HasMode(os.ModeSymlink) } + +// IsDevice retourne vrai si le fichier est un fichier de périphérique. +func (f *File) IsDevice() bool { return f.HasMode(os.ModeDevice) } + +// IsCharDevice retourne vrai si le fichier est un fichier de périphérique de caractère. +func (f *File) IsCharDevice() bool { return f.HasMode(os.ModeCharDevice) } + +// IsSocket retourne vrai si le fichier est un socket. +func (f *File) IsSocket() bool { return f.HasMode(os.ModeSocket) } + +// IsPipe retourne vrai si le fichier est de type pipe. +func (f *File) IsPipe() bool { return f.HasMode(os.ModeNamedPipe) } + +// IsIrregular retourne vrai si fichier est de type inconnu. +func (f *File) IsIrregular() bool { return f.HasMode(os.ModeIrregular) } + +// IsRegular retourne vrai si le fichier est un fichier régulier. +func (f *File) IsRegular() bool { return f.Mode().IsRegular() } + +// IsUid retourne vrai si le fichier a la permission Suid. +func (f *File) IsUid() bool { return f.HasMode(os.ModeSetuid) } + +// IsGid retourne vrai si le fichier a la permission Sgid. +func (f *File) IsGid() bool { return f.HasMode(os.ModeSetgid) } + +// IsSticky retourne vrai si le fichier a la permission Sticky Bit. +func (f *File) IsSticky() bool { return f.HasMode(os.ModeSticky) } + +// Name retourne le nom du fichier. +func (f *File) Name() string { return f.name } + +// DirName retourne le répertoire d’appartenance. +func (f *File) DirName() string { return f.dirname } + +// Path retourne le chemin complet du fichier. +func (f *File) Path() string { return filepath.Join(f.dirname, f.name) } + +// AbsolutePath agit comme Path mais s’assure que le chemin soit absolu. +func (f *File) AbsolutePath() string { + path := f.Path() + if filepath.IsAbs(path) { + return path + } + + abs, _ := filepath.Abs(path) + return abs +} + +// Split retourne le dossier et le nom du fichier. +func (f *File) Split() (dirname, name string) { + path := filepath.Clean(f.Path()) + + return filepath.Split(path) +} + +// Split retourne le dossier absolu et le nom du fichier. +func (f *File) AbsoluteSplit() (dirname, name string) { + path := f.AbsolutePath() + + return filepath.Split(path) +} + +// Extension retourne l’extension du fichier. +func (f *File) Extension() string { + if f.IsDir() { + return "" + } + + return filepath.Ext(f.info.Name()) +} + +// LinkPath retourne le chemin du fichier pointé +// par le lien symbolique, ou une chaîne vide si le lien est cassé +// ou que le fichier n’est pas un lien symbolique. +func (f *File) LinkPath() string { + if !f.IsSymlink() { + return "" + } + + lpath, _ := os.Readlink(f.Path()) + return lpath +} + +func ts2Date(ts syscall.Timespec) time.Time { return time.Unix(ts.Sec, ts.Nsec) } + +// CreationTime retourne la date de création du fichier. +func (f *File) CreationTime() time.Time { return ts2Date(f.stat().Ctim) } + +// AccessTime retourne la date d’accès du fichier. +func (f *File) AccessTime() time.Time { return ts2Date(f.stat().Atim) } + +// ModificationTime retourne la date de modification du fichier. +func (f *File) ModificationTime() time.Time { return f.info.ModTime() } + +// Size retourne la taille (en octets) du fichier. +func (f *File) Size() int64 { return f.info.Size() } + +// BlockSize retourne la taille de blocs (en octets) du fichier. +func (f *File) BlockSize() int64 { return f.stat().Blksize } + +// DirSize retourne la taille totale des fichiers (en octets) contenus dans un répertoire, +// ou bien la taille du fichier s’il s’agit d’un simple fichier. +// La taille inclut de façon récursive celle des sous-répertoires. +func (f *File) DirSize() (size int64) { + if !f.IsDir() { + return f.Size() + } + + for _, c := range f.children { + size += c.DirSize() + } + + return +} + +// OwnerID retourne l’ID utilisateur du fichier. +func (f *File) OwnerID() string { return strconv.Itoa(int(f.stat().Uid)) } + +// GroupID retourne l’ID du groupe du fichier. +func (f *File) GroupID() string { return strconv.Itoa(int(f.stat().Gid)) } + +// Owner retourne le nom du propriétaire du fichier. +func (f *File) Owner() string { + id := f.OwnerID() + if owner, ok := owners[id]; ok { + return owner + } + + var owner string + if e, err := user.LookupId(id); err == nil { + owner = e.Name + } + owners[id] = owner + + return owner +} + +// Group retourne le nom du groupe du fichier. +func (f *File) Group() string { + id := f.GroupID() + if group, ok := groups[id]; ok { + return group + } + + var group string + if e, err := user.LookupGroupId(id); err == nil { + group = e.Name + } + groups[id] = group + + return group +} + +// Children retourne la liste des fichiers de premier niveau du répertoire +// ou rien si le fichier n’est pas un répertoire. +func (f *File) Children() FileList { return f.children } + +// SearchChildren recherche les fichiers d’un répertoire. +// deepness indique la profondeur de recherche et doit valoir au moins 1. +// searchoptions peut contenir deux options de recherche optionnelles : +// 1. Fichiers masqués (par défaut non retournés), +// 2. Fichiers de backup (ie. ceux avec le suffixe ~) (par défaut non retournés). +func (f *File) SearchChildren(deepness int, searchOptions ...bool) (children FileList) { + if !f.IsDir() || deepness == 0 { + return + } + + var hidden, backup bool + if len(searchOptions) > 0 { + hidden = searchOptions[0] + if len(searchOptions) > 1 { + backup = searchOptions[1] + } + } + + dir, err := os.Open(f.Path()) + if err != nil { + return + } + defer dir.Close() + + infos, _ := dir.Readdir(0) + dirname := f.Path() + if deepness > 0 { + deepness-- + } + + var wg sync.WaitGroup + for _, i := range infos { + name := i.Name() + if (!hidden && strings.HasPrefix(name, ".")) || (!backup && strings.HasSuffix(name, "~")) { + continue + } + child := NewFromInfo(dirname, i) + children.Add(child) + wg.Add(1) + go (func(c *File) { + defer wg.Done() + c.SearchChildren(deepness, searchOptions...) + })(child) + } + wg.Wait() + + f.children = children + for _, c := range children { + c.parent = Some(f) + } + + return +} + +// Flatten retourne la liste de tous les fichiers et répertoires de façon récursive. +// Si only_dir est présent et vaut true, seuls les répertoires sont retournés. +func (f *File) Flatten(only_dirs ...bool) (fl FileList) { + d := len(only_dirs) > 0 && only_dirs[0] + if d && !f.IsDir() { + return + } + + fl.Add(f) + for _, c := range f.children { + fl.Add(c.Flatten(only_dirs...)...) + } + + return +} + +// Parent retourne le répertoire d’appartenance. +func (f *File) Parent() Option[*File] { + return f.parent +} + +// SearchParent agit comme Parent mais force la recherche +// si celui-ci n’est pas encore défini. +func (f *File) SearchParent() Option[*File] { + if f.parent.IsDefined() { + return f.parent + } + + d, _ := f.AbsoluteSplit() + pdirname, pname := filepath.Split(d) + p := New(pdirname, pname) + if parent, ok := p.Ok(); ok { + f.parent = Some(parent) + } + + return f.parent +} + +// AddChildren ajoute des fichiers en tant qu’appartenance à un répertoire. +func (f *File) AddChildren(children ...*File) { + for _, c := range children { + c.parent = Some(f) + } + f.children.Add(children...) +} + +// SetParent définit le répertoire parent du fichier. +// Si both est donné et est vrai, le fichier est ajouté au répertoire +// parent en tant qu’enfant. +func (f *File) SetParent(parent Option[*File], both ...bool) { + f.parent = parent + if p, ok := parent.Get(); ok && len(both) > 0 && both[0] { + p.children.Add(f) + } +} diff --git a/shell/file/filelist.go b/shell/file/filelist.go new file mode 100644 index 0000000..1f731ac --- /dev/null +++ b/shell/file/filelist.go @@ -0,0 +1,188 @@ +package file + +import ( + "cmp" + "sort" + "strings" + "time" + + "gitea.zaclys.com/bvaudour/gob/compare" +) + +// FileList représente une liste de fichiers. +type FileList []*File + +func (fl *FileList) Add(files ...*File) { *fl = append(*fl, files...) } + +// CmpFunc est une fonction comparant deux répertoires. +// Elle peut être utilisée pour trier des répertoires. +type CmpFunc func(*File, *File) int + +// CmpAll agrège plusieurs fonctions de comparaison +// en une seule. +func CmpAll(args ...CmpFunc) CmpFunc { + return func(f1, f2 *File) int { + for _, cb := range args { + if c := cb(f1, f2); c != 0 { + return c + } + } + return 0 + } +} + +// ReverseCmp inverse la fonction de comparaison. +func ReverseCmp(cb CmpFunc) CmpFunc { + return func(f1, f2 *File) int { + return -cb(f1, f2) + } +} + +// Sort trie la liste de fichier selon la fonction de comparaison. +func (fl FileList) Sort(cb CmpFunc) FileList { + if cb == nil { + return fl + } + less := func(i, j int) bool { return cb(fl[i], fl[j]) < 0 } + sort.Slice(fl, less) + return fl +} + +func tr(e1, e2 string, t compare.TransformFunc) []string { return []string{t(e1), t(e2)} } + +func trAll(e1, e2 string) [][]string { + a := tr(e1, e2, compare.ToAscii) + ai := tr(a[0], a[1], compare.ToLower) + i := tr(e1, e2, compare.ToLower) + return [][]string{ai, a, i, []string{e1, e2}} +} + +func trf(f1, f2 *File) (h1, h2 bool, data [][]string) { + n1, n2 := f1.name, f2.name + h1, h2 = len(n1) > 0 && n1[0] == '.', len(n2) > 0 && n2[0] == '.' + if h1 { + n1 = n1[1:] + } + if h2 { + n2 = n2[1:] + } + data = trAll(n1, n2) + return +} + +func cmpStrings(data [][]string, cb compare.CompareFunc) int { + for _, d := range data { + if c := cb(d[0], d[1]); c != 0 { + return c + } + } + return 0 +} + +func cmpRoot(f1, f2 *File) (int, bool) { + switch { + case f1.name == f2.name: + return 0, true + case f1.name == ".": + return -1, true + case f2.name == ".": + return 1, true + case f1.name == "..": + return -1, true + case f2.name == "..": + return 1, true + } + return 0, false +} + +func cmpHidden(h1, h2 bool) int { + if h1 != h2 { + if h1 { + return -1 + } + return 1 + } + return 0 +} + +func cmpInt(cb func(*File) int64) CmpFunc { + return func(f1, f2 *File) int { + e1, e2 := cb(f1), cb(f2) + + return cmp.Compare(e1, e2) + } +} + +func cmpDate(cb func(*File) time.Time) CmpFunc { + return func(f1, f2 *File) int { + e1, e2 := cb(f1), cb(f2) + + return cmp.Compare(e1.Unix(), e2.Unix()) + } +} + +func cmpName(cb compare.CompareFunc) CmpFunc { + return func(f1, f2 *File) int { + if c, ok := cmpRoot(f1, f2); ok { + return c + } + h1, h2, data := trf(f1, f2) + if c := cmpStrings(data, cb); c != 0 { + return c + } + + return cmpHidden(h1, h2) + } +} + +// CmpDir place les répertoires en premier. +func CmpDir(f1, f2 *File) int { + d1, d2 := f1.IsDir(), f2.IsDir() + + if d1 == d2 { + return 0 + } else if d1 { + return -1 + } + return 1 +} + +// CmpExt compare deux fichiers de même nom par leur extension. +func CmpExt(f1, f2 *File) int { + return compare.CompareInsensitive(f1.Extension(), f2.Extension()) +} + +// CmpSize compare deux fichiers par leur taille. +func CmpSize(f1, f2 *File) int { + return cmpInt(func(f *File) int64 { return f.Size() })(f1, f2) +} + +// CmpBlockSize compare deux fichiers par leur taille de bloc. +func CmpBlockSize(f1, f2 *File) int { + return cmpInt(func(f *File) int64 { return f.BlockSize() })(f1, f2) +} + +// CmpCreationTime compare deux fichiers par leur date de création. +func CmpCreationTime(f1, f2 *File) int { + return cmpDate(func(f *File) time.Time { return f.CreationTime() })(f1, f2) +} + +// CmpAccessTime compare deux fichiers par leur date d’accès. +func CmpAccessTime(f1, f2 *File) int { + return cmpDate(func(f *File) time.Time { return f.AccessTime() })(f1, f2) +} + +// CmpModificationTime compare deux fichiers par leur date de modification. +func CmpModificationTime(f1, f2 *File) int { + return cmpDate(func(f *File) time.Time { return f.ModificationTime() })(f1, f2) +} + +// CmpName compare deux fichiers par leur nom. +func CmpName(f1, f2 *File) int { + return cmpName(strings.Compare)(f1, f2) +} + +// CmpNatural compare deux fichier par leur nom de manière naturelle. +func CmpNatural(f1, f2 *File) int { + return cmpName(func(e1, e2 string) int { return compare.Natural(e1, e2) })(f1, f2) +} diff --git a/shell/scanner/scanner.go b/shell/scanner/scanner.go new file mode 100644 index 0000000..8d7eaad --- /dev/null +++ b/shell/scanner/scanner.go @@ -0,0 +1,125 @@ +package scanner + +import ( + "bufio" + "errors" + "io" + "unicode/utf8" + + "gitea.zaclys.com/bvaudour/gob/collection" +) + +// Tokenizer est une interface implémentant une fonction de splittage. +type Tokenizer interface { + Split(data []byte, atEOF bool) (advance int, token []byte, err error) +} + +type tk struct { + quote bool + escape bool + spaces []rune +} + +func runeToBytes(r rune) []byte { + l := utf8.RuneLen(r) + b := make([]byte, l) + utf8.EncodeRune(b, r) + return b +} + +func (t tk) Split(data []byte, atEOF bool) (advance int, token []byte, err error) { + q, e := collection.NewSet[rune](), collection.NewSet[rune]() + s := collection.NewSet(t.spaces...) + if t.quote { + q.Add('\'', '"') + } + if t.escape { + e.Add('\\') + } + + // Skip leading spaces. + start := 0 + for width := 0; start < len(data); start += width { + var r rune + r, width = utf8.DecodeRune(data[start:]) + if !s.Contains(r) { + break + } + } + + var quote, esc rune + + // Scan until space, marking end of word. + for width, i := 0, start; i < len(data); i += width { + var r rune + r, width = utf8.DecodeRune(data[i:]) + if s.Contains(r) && !q.Contains(quote) && !e.Contains(esc) { + return i + width, token, nil + } + if e.Contains(esc) { + if q.Contains(quote) && !e.Contains(r) && r != quote { + token = append(token, runeToBytes(esc)...) + } + token = append(token, runeToBytes(r)...) + esc = 0 + } else if e.Contains(r) { + esc = r + } else if q.Contains(r) { + if !q.Contains(quote) { + quote = r + } else if quote == r { + quote = 0 + } else { + token = append(token, runeToBytes(r)...) + } + } else { + token = append(token, runeToBytes(r)...) + } + } + // If we're at EOF, we have a final, non-empty, non-terminated word. Return it. + if atEOF && len(data) > start { + if e.Contains(esc) || q.Contains(quote) { + return start, nil, errors.New("Incomplete token") + } + return len(data), token, nil + } + // Request more data. + return start, nil, nil +} + +// NewTokenizer retourne un tokenizer d’arguments de ligne de commande. +// +// Le split s’effectue au niveau des caractères spaces fourni (tous les espaces +// si aucun de fourni) sauf si ces caractères sont échappés +// (si escape) ou entre guillemets (si quote) +// Par exemple, prenons la chaîne suivante : +// unmot une\ phrase "une deuxième\" phrase" +// +// Le résultat va être décomposé en 3 éléments : +// - unmot +// - une\ phrase +// - "une deuxième\" phrase" +func NewTokenizer(quote, escape bool, spaces ...rune) Tokenizer { + if len(spaces) == 0 { + spaces = []rune(" \t\n\v\f\r") + } + return tk{ + quote: quote, + escape: escape, + spaces: spaces, + } +} + +// NewScanner retourne un nouveau scanner utilisant le tokenizer spécifié +// pour la fonction de splitage (NewTokenizer(true, true) si aucun tokenizer fourni). +func NewScanner(r io.Reader, t ...Tokenizer) *bufio.Scanner { + var s bufio.SplitFunc + if len(t) > 0 { + s = t[0].Split + } else { + s = (NewTokenizer(true, true)).Split + } + sc := bufio.NewScanner(r) + sc.Split(s) + return sc +}