diff --git a/README.md b/README.md index 94bc4cf..f5f9893 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,65 @@ # elv-lib +elv-lib includes some useful module functions and completions + + + +## mods + +This folder groups modules for different usages: + +### file + +file provides functions to complete a filename/directory motive with different methods: + +* exact motive +* contains motive (with or without deep search) + +- search by prefix (with or without deep search) +- search by suffix (with or without deep search) +- filter results by given extension + +### format + +format provides functions to beautify a given input. Examples: + +- format:size returns a size (in bytes) in a human-readable format +- format:list displays data in columns + +### ip + +ip provides useful functions to manipulate IPv4 or IPv6 addresses. + +### list + +list provides functions to manipulate data of type list. Examples: + +* list:foreach acts as builtin each with the callback function on the form {|i v| } where i is the index in the list an v the value +* list:reach acts as builtin each in reverse order +* list:filter filters in a list with the given callback criterium +* list:contains check if a list contains a specific entry or an entry which passes a callback +* etc. + +### map + +map provides functions to manipulate data of type map. + +### num + +num provides useful functions for number operations which are not part of builtin functions or math: module functions. + +### option + +### pdf + +pdf provides functions to manipulate PDF files (qpdf is required to make this module work). + +## completion + +The folder completion contains the completion for some programs: + +- [arc](https://github.com/mholt/archiver) +- [kcp](https://github.com/bvaudour/kcp) +- mpv +- pacman +- ssh diff --git a/alias.elv b/alias.elv new file mode 100644 index 0000000..ad1e735 --- /dev/null +++ b/alias.elv @@ -0,0 +1,24 @@ +var dir = $E:HOME/.config/elvish/aliases + +for file [(set _ = ?(put $dir/*.elv))] { + var content = (cat $file | slurp) + eval $content +} + +{ + use ./mods/common + edit:add-var cond~ $common:cond~ + edit:add-var cexec~ $common:cexec~ +} + +{ + use ./mods/num + edit:add-var '++~' $'num:++~' + edit:add-var '--~' $'num:--~' + edit:add-var neg~ $num:neg~ + edit:add-var is-neg~ $num:is-neg~ + edit:add-var is-zero~ $num:is-zero~ + edit:add-var is-one~ $num:is-one~ + edit:add-var min~ $num:min~ + edit:add-var max~ $num:max~ +} diff --git a/completion.elv b/completion.elv new file mode 100644 index 0000000..a927e4b --- /dev/null +++ b/completion.elv @@ -0,0 +1,9 @@ +use ./completion/arc +use ./completion/archiver +use ./completion/desarchiver +use ./completion/kcp +use ./completion/mpv +use ./completion/pacman +use ./completion/ssh +use ./completion/sudo +use ./completion/use diff --git a/completion/arc.elv b/completion/arc.elv new file mode 100644 index 0000000..37c8cff --- /dev/null +++ b/completion/arc.elv @@ -0,0 +1,53 @@ +use ./file + +var commands = [ + help + archive + unarchive + extract + ls +] + +var description = [ + &help='Display the help' + &archive='Create un new archive' + &unarchive='Extract all' + &extract='Extract a single file' + &ls='List the content' +] + +var extensions = [ tar bz2 zip gz lz4 sz xz zst rar ] + +fn -comp-commands { + each {|c| + edit:complex-candidate $c &display=(printf '%s (%s)' $c $description[$c]) + } $commands +} + +fn -comp-inline-files {|archive| + try { + arc ls $archive | eawk {|_ @argv| put $argv[-1] } + } except e { + nop + } +} + +fn complete {|@argv| + var c cmd = (count $argv) $argv[1] + if (== $c 2) { + -comp-commands + } elif (== $c 3) { + if (not (has-value [help archive] $cmd)) { + file:complete $argv[-1] $@extensions + } + } else { + if (eq $cmd archive) { + edit:complete-filename $@argv + } elif (eq $cmd extract) { + var archive = $argv[2] + -comp-inline-files $archive + } + } +} + +set edit:completion:arg-completer[arc] = $complete~ diff --git a/completion/archiver.elv b/completion/archiver.elv new file mode 100644 index 0000000..3cef6c5 --- /dev/null +++ b/completion/archiver.elv @@ -0,0 +1,29 @@ +var options = [ + z + l + x + b + g + h +] + +var description = [ + &z='zst compression (default)' + &l='lz4 compression' + &x='xz compression' + &b='bz2 compression' + &g='gzip compression' + &h='display help' +] + +fn -options { + each {|o| + put [&short=$o &desc=$description[$o]] + } $options +} + +fn complete {|@argv| + edit:complete-getopt $argv [(-options)] [ $edit:complete-filename~ ...] +} + +set edit:completion:arg-completer[archiver] = $complete~ diff --git a/completion/desarchiver.elv b/completion/desarchiver.elv new file mode 100644 index 0000000..6b9c406 --- /dev/null +++ b/completion/desarchiver.elv @@ -0,0 +1,10 @@ +use ./file + +var extensions = [ tar bz2 zip gz lz4 sz xz zst rar ] + +fn complete {|@argv| + var m = $argv[-1] + file:complete $m $@extensions +} + +set edit:completion:arg-completer[desarchiver] = $complete~ diff --git a/completion/file.elv b/completion/file.elv new file mode 100644 index 0000000..84a1544 --- /dev/null +++ b/completion/file.elv @@ -0,0 +1,7 @@ +use ../mods/common +use ../mods/file + +fn complete {|motive @extensions| + var type = (common:cond (eq $motive '') prefix deep-prefix) + file:match-extensions &type=$type $motive $@extensions +} diff --git a/completion/kcp.elv b/completion/kcp.elv new file mode 100644 index 0000000..8a8da0b --- /dev/null +++ b/completion/kcp.elv @@ -0,0 +1,41 @@ +fn -remotes-packages { kcp -lN } + +var options = [ + -h + -v + -i + -di + -u + -l + -lN + -lS + -lI + -lO + -lx + -lxS + -lxI + -lxO + -lf + -s + -g + -V +] + +var np = [ + -i + -di + -s + -g + -V +] + +fn complete {|@argv| + var c = (count $argv) + if (== $c 2) { + all $options + } elif (and (== $c 3) (has-value $np $argv[-2])) { + -remotes-packages + } +} + +set edit:completion:arg-completer[kcp] = $complete~ diff --git a/completion/mpv.elv b/completion/mpv.elv new file mode 100644 index 0000000..1758337 --- /dev/null +++ b/completion/mpv.elv @@ -0,0 +1,53 @@ +use ./file + +var extensions = [ + aac + ape + avi + divx + flac + flv + m3u + m4a + m4v + mp3 + mp4 + mpeg + mpg + mkv + mng + mov + qt + oga + ogg + ogm + ogv + opus + ra + rv + ts + vob + wav + webm + wmv + wma + wmx +] + +fn complete {|@argv| + var c = (count $argv) + if (== $c 2) { + put --speed + -files $argv[-1] + } elif (== $c 3) { + if (eq $argv[-2] --speed) { + put 0.8 0.9 1.0 1.1 1.2 + } else { + file:complete $argv[-1] $@extensions + } + } else { + -files $argv[-1] + } +} + +set edit:completion:arg-completer[mpv] = $complete~ diff --git a/completion/pacman.elv b/completion/pacman.elv new file mode 100644 index 0000000..b1c296a --- /dev/null +++ b/completion/pacman.elv @@ -0,0 +1,99 @@ +use str +use ../mods/file +use ../mods/list + +fn -local-packages { pacman -Q | eawk {|_ p @_| put $p } } + +fn -repo-packages { + var packages = [(pacman -Ss | list:pforeach &step=2 {|_ v| + put $v + } | eawk {|_ p @_| + put $p + })] + var spackages = [&] + peach {|p| + str:split '/' $p + } $packages | peach {|e| + set spackages[$e] = $nil + } + keys $spackages + all $packages +} + +var options = [ + -h + -V + -Q + -Qs + -Ql + -Qi + -Qm + -Qdt + -Qo + -R + -Rsn + -S + -Ss + -Si + -Sii + -Syu + -Syyu + -U + -D +] + +var asdeps = [ + -S + -U + -D +] + +var lpack = [ + -Q + -Qs + -Ql + -Qi + -D + -R + -Rsn +] + +var rpack = [ + -S + -Ss + -Si + -Sii +] + +var dpack = [ + -U +] + +var fpack = [ + -Qo +] + +var extensions = [ tar.zst tar.xz tar.gz tar.bz2 ] + +fn complete {|@argv| + var c = (count $argv) + if (< $c 3) { + all $options + } else { + var cmd = $argv[1] + if (and (== $c 3) (has-value $asdeps $cmd)) { + put --asdeps --asexplicit + } + if (has-value $lpack $cmd) { + -local-packages + } elif (has-value $rpack $cmd) { + -repo-packages + } elif (has-value $dpack $cmd) { + file:complete $argv[-1] $@extensions + } elif (has-value $fpack $cmd) { + edit:complete-filename $argv[-1] + } + } +} + +set edit:completion:arg-completer[pacman] = $complete~ diff --git a/completion/ssh.elv b/completion/ssh.elv new file mode 100644 index 0000000..636abf6 --- /dev/null +++ b/completion/ssh.elv @@ -0,0 +1,95 @@ +use path +use re +use str +use ../mods/common +use ../mods/list +use ../mods/map +use ../mods/option + +var options-ssh = [ 1 2 4 6 A D f g I i L l m o v a b C c e F k N n p q R s T t X x ] +var options-scp = [ 3 4 6 B C p q r v c F i l o P S ] + +fn -kh { + cat ~/.ssh/known_hosts | peach {|l| + put [(str:split ' ' $l)] + } | peach {|e| + var domains @_ = $@e + str:split ',' $domains + } +} + +fn -port {|cmd @argv| + var o = (common:cond (eq $cmd 'ssh') '-p' '-P') + var margs = (option:map $argv) + var p = (map:value-of $margs $o &default=[]) + cond:cexec (list:is-empty $p) 22 { put $p[-1] } +} + +fn -complete-names { + var fp = $E:HOME/.config/elvish/private/sshnames + if (path:is-regular $fp) { + cat $fp | from-lines | each {|n| put (printf '%s@' $n) } + } +} + +fn -complete-domains {|name hosts| + each {|h| + put (printf '%s@%s' $name $h) + } $hosts +} + +fn -complete-remote-dir {|port address dir| + var cmd = (printf ^ + "for f in '%s'*; do if [[ -d $f ]]; then echo $f/; else echo $f; fi; done" ^ + $dir) + try { + ssh -p $port $address $cmd | each {|f| + put (printf '%s:%s' $address $f) + } + } except e { } +} + +fn -complete-args {|hosts cmd @argv| + var arg = $argv[-1] + var i = (str:index $arg @) + if (< $i 0) { + -complete-names + all $hosts + if (eq $cmd scp) { + edit:complete-filename $cmd $@argv + } + return + } + var n h = $arg[..$i] $arg[(+ $i 1)..] + if (eq $cmd scp) { + set i = (str:index $h :) + if (>= $i 0) { + var d = $h[(+ $i 1)..] + set h = $h[..$i] + if (list:contains $h $hosts) { + var p = (-port $cmd @argv) + -complete-remote-dir $p $n@$h $d + } + return + } + } + -complete-domains $n $hosts +} + +fn complete {|@argv| + var @hosts = (-kh) + var cmd = $argv[0] + var is-ssh = (eq $cmd ssh) + var po = (common:cond $is-ssh -p -P) + if (<= (count $argv) 2) { + all (common:cond $is-ssh $options-ssh $options-scp) + -complete-args $hosts $@argv + } elif (eq $argv[-2] $po) { + put 22 + } else { + -complete-args $hosts $@argv + } +} + +set edit:completion:arg-completer[scp] = $complete~ +set edit:completion:arg-completer[ssh] = $complete~ diff --git a/completion/sudo.elv b/completion/sudo.elv new file mode 100644 index 0000000..3d95d63 --- /dev/null +++ b/completion/sudo.elv @@ -0,0 +1,10 @@ +fn complete {|@argv| + if (and (> (count $argv) 2) (has-key $edit:completion:arg-completer $argv[1])) { + $edit:completion:arg-completer[$argv[1]] (all $argv[1:]) + } else { + edit:complete-sudo $@argv + } +} + +set edit:completion:arg-completer[sudo] = $edit:complete-sudo~ +#edit:completion:arg-completer[sudo] = $-complete~ diff --git a/completion/use.elv b/completion/use.elv new file mode 100644 index 0000000..a0af5d5 --- /dev/null +++ b/completion/use.elv @@ -0,0 +1,31 @@ +use path +use str +use ./file + +var libdir = $E:HOME/.config/elvish/lib +var builtin_modules = [ + builtin + epm + file + math + path + re + readline-binding + store + str + unix +] + +set edit:completion:arg-completer[use] = {|@argv| + use str + use path + all $builtin_modules + put $libdir/**.elv | each {|f| + if (path:is-regular $f) { + str:trim-prefix $f $libdir/ + } + } | each {|f| str:trim-suffix $f .elv } + if (> (count $argv) 1) { + file:complete $argv[-1] elv | each {|f| str:trim-suffix $f .elv } + } +} diff --git a/mods/common.elv b/mods/common.elv new file mode 100644 index 0000000..f5843d3 --- /dev/null +++ b/mods/common.elv @@ -0,0 +1,27 @@ +fn to-list {|@argv| + var c = (count $argv) + if (== $c 0) { + put [ (all) ] + } elif (== $c 1) { + put $argv[0] + } else { + fail (printf '0 or 1 argument needed given %d arguments: %v' $c $argv) + } +} + +fn cond {|cond v1 v2| + if $cond { + put $v1 + } else { + put $v2 + } +} + +fn cexec {|cond v1 v2| + var r = (cond $cond $v1 $v2) + if (eq (kind-of $r) fn) { + $r + } else { + put $r + } +} diff --git a/mods/file.elv b/mods/file.elv new file mode 100644 index 0000000..7b51a1f --- /dev/null +++ b/mods/file.elv @@ -0,0 +1,61 @@ +use str +use ./list +use ./map + +var search-type = [ + &exact= {|m| ls $m 2>/dev/null } + &match= {|m| put *$m* } + &prefix= {|m| put $m* } + &suffix= {|m| put *$m } + &deep-match= {|m| put **$m** } + &deep-prefix= {|m| put $m** } + &deep-suffix= {|m| put **$m } +] + +fn comp {|f1 f2| + var fl1 fl2 = (str:to-lower $f1) (str:to-lower $f2) + var c = (str:compare $fl1 $fl2) + if (!= $c 0) { + put $c + } else { + str:compare $f1 $f2 + } +} + +fn -r {|sort result| + if $sort { + keys $result | list:sort $comp~ + } else { + keys $result + } +} + +fn -s {|&sort=$false &type=exact @motive| + var f result = $search-type[$type] [&] + if (list:is-empty $motive) { + set result = (put * | map:to-set) + } else { + peach {|m| + try { + $f $m | peach {|e| + set result[$e] = $nil + } + } except e { } + } $motive + } + -r $sort $result +} + +fn match {|&sort=$false &type=prefix @motive| + -s &sort=$sort &type=$type $@motive +} + +fn match-extensions {|&sort=$false &type=deep-prefix motive @extensions| + var result = [&] + -s &type=$type $motive | peach {|f| + if (list:contains {|e| str:has-suffix $f .$e } $extensions) { + set result[$f] = $nil + } + } + -r $sort $result +} diff --git a/mods/format.elv b/mods/format.elv new file mode 100644 index 0000000..ffebb78 --- /dev/null +++ b/mods/format.elv @@ -0,0 +1,198 @@ +use math +use re +use str +use ./common + +fn chars {|v| + str:split '' $v +} + +fn len {|v| + chars $v | count +} + +fn string {|v| + var k = (kind-of $v) + if (eq $k string) { + put $v + } elif (eq $k number) { + to-string $v + } elif (eq $k bool) { + common:cond $v 'X' '' + } elif (eq $k list) { + each $string~ $v | str:join '' + } elif (eq $v $nil) { + put '' + } else { + to-string $v + } +} + +fn int {|v| + var k = (kind-of $v) + if (eq $k 'string') { + try { + var n = (math:trunc $v) + var s @_ = (str:split '.' (to-string $n)) + put $s + } except e { + fail (printf '%s n’est pas un nombre' $v) + } + } elif (eq $k 'number') { + int (to-string $v) + } elif (eq $k 'bool') { + common:cond $v 1 0 + } else { + put 0 + } +} + +fn repeat {|n s| + use builtin + builtin:repeat $n $s | str:join '' +} + +fn blank {|n| + repeat $n ' ' +} + +fn left {|str size| + var l = (len $str) + if (< $l $size) { + string [ $str (blank (- $size $l)) ] + } elif (== $l $size) { + put $str + } else { + string [ (chars $str) ][..$size] + } +} + +fn right {|str size| + var l = (len $str) + if (< $l $size) { + string [ (blank (- $size $l)) $str ] + } elif (== $l $size) { + put $str + } else { + string [ (chars $str) ][(- $l $size)..] + } +} + +fn center {|str size| + var l = (len $str) + if (< $l $size) { + var d = (- $size $l) + var b = (math:trunc (/ $d 2)) + var e = (- $d $b) + string [ (blank $b) $str (blank $e) ] + } elif (== $l $size) { + put $str + } else { + var d = (- $l $size) + var b = (math:trunc (/ $d 2)) + var e = (+ $b $size) + string [ (chars $str) ][$b..$e] + } +} + +var -align = [ + ¢er=$center~ + &right=$right~ + &left=$left~ +] + +fn column {|&align=left name @label| + var c = [ + &name=$name + &align=$-align[$align] + &size=0 + &label='' + ] + if (> (count $label) 0) { + set c[label] = $label[0] + set c[size] = (count $label[0]) + } + put $c +} + +fn update-props {|props data| + each {|d| + set @props = (each {|c| + var n = $c[name] + if (has-key $d $n) { + var v = (string $d[$n]) + var l = (count $v) + if (< $c[size] $l) { + set c[size] = $l + } + } + put $c + } $props) + } $data + put $props +} + +fn line-header {|&sep=' ' props| + var @data = (each {|c| + $c[align] $c[label] $c[size] + } $props) + str:join $sep $data +} + +fn line {|&sep=' ' props line| + var @data = (each {|c| + var n v = $c[name] '' + if (has-key $line $n) { + set v = (string $line[$n]) + } + $c[align] $v $c[size] + } $props) + str:join $sep $data +} + +fn list {|&with-header=$true &csep=' ' &hsep='-' &recompute=$true props data| + if $recompute { + set props = (update-props $props $data) + } + if $with-header { + echo (line-header &sep=$csep $props) + if (bool $hsep) { + var s = (* (- (count $props) 1) (count $csep)) + each {|c| + set s = (+ $s $c[size]) + } $props + echo (repeat (math:trunc (/ $s (count $hsep))) $hsep) + } + } + each {|d| + echo (line &sep=$csep $props $d) + } $data +} + +fn size {|size| + var u = 0 + var m = [ + &10=Kio + &20=Mio + &30=Gio + &40=Tio + &50=Pio + &60=Eio + &70=Zio + &80=Yio + ] + while (< $u 80) { + var p = (math:pow 2 (+ $u 10)) + if (< $size $p) { + break + } + set u = (to-string (+ $u 10)) + } + if (== $u 0) { + put $size + } else { + var p = (math:pow 2 $u) + var e = (/ $size $p) + printf '%.1f%s' $e $m[$u] + } +} diff --git a/mods/ip.elv b/mods/ip.elv new file mode 100644 index 0000000..32e7b5c --- /dev/null +++ b/mods/ip.elv @@ -0,0 +1,154 @@ +use re +use str +use ./common +use ./list +use ./num + +fn is-ipv4 {|arg| + common:cexec ^ + (re:match '^([0-9]{1,3}\.){3}[0-9]{1,3}$' $arg) ^ + { not (str:split '.' $arg | list:contains {|p| > $p 255 }) } ^ + $false +} + +fn is-ipv6 {|arg| + var p = '[0-9a-fA-F]{1,4}' + var g1 g2 = (printf '(%s:)' $p) (printf '(:%s)' $p) + var cases = [ + (printf '%s{7,7}%s' $g1 $p) + (printf '%s{1,7}:' $g1) + (printf '%s{1,6}:%s' $g1 $p) + (printf '%s{1,5}%s{1,2}' $g1 $g2) + (printf '%s{1,4}%s{1,3}' $g1 $g2) + (printf '%s{1,3}%s{1,4}' $g1 $g2) + (printf '%s{1,2}%s{1,5}' $g1 $g2) + (printf '%s:%s{1,6}' $p $g2) + (printf ':%s{1,7}' $g2) + '::' + ] + var r = (printf '^(%s)$' (str:join '|' $cases)) + re:match $r $arg +} + +fn is-ip {|arg| + or (is-ipv4 $arg) (is-ipv6 $arg) +} + +fn -l6 {|p| + set p = (str:to-lower $p) + var c = (- 4 (count $p)) + put (repeat $c '0') $p | str:join '' +} + +fn -m6 {|p| + while (and (> (count $p) 1) (eq $p[0] 0)) { + set p = $p[1..] + } + put $p +} + +fn -max0 {|parts| + var idx s = -1 1 + var ci cs f = -1 0 $false + list:foreach {|i p| + if (eq $p 0) { + if $f { + set cs = (num:++ $cs) + } else { + set ci cs f = $i 1 $true + } + } elif $f { + set f = $false + if (> $cs $s) { + set idx s = $ci $cs + } + } + } $parts + if (and $f (> $cs $s)) { + set idx s = $ci $cs + } + put $idx $s +} + +fn long6 {|ip| + if (not (is-ipv6 $ip)) { + fail 'Not an IPv6' + } + if (eq $ip '::') { + repeat 8 '0000' | str:join ':' + return + } + var c = (- 7 (str:count $ip ':')) + if (> $c 0) { + var i = (str:index $ip '::') + var z = (repeat $c ':' | str:join '') + set ip = (str:join '' [$ip[..$i] $z $ip[$i..]]) + } + str:split ':' $ip | each $-l6~ | str:join ':' +} + +fn middle6 {|ip| + str:split ':' (long6 $ip) | each $-m6~ | str:join ':' +} + +fn short6 {|ip| + var parts = [(str:split ':' (middle6 $ip))] + var i s = (-max0 $parts) + if (>= $i 0) { + var left right = $parts[..$i] $parts[(+ $i $s)..] + if (== (count $left) 0) { + set left = [''] + } + if (== (count $right) 0) { + set right = [''] + } + set @parts = $@left '' $@right + } + str:join ':' $parts +} + +fn -cmp {|e1 e2| + var c = 0 + list:foreach {|i p1| + var p2 = $e2[$i] + set c = (compare $p1 $p2) + if (!= $c 0) { + break + } + } $e1 + put $c +} + +fn comp4 {|ip1 ip2| + if (or (not (is-ipv4 $ip1)) (not (is-ipv4 $ip2))) { + fail (printf 'Not an IPv4: %s → %s' $ip1 $ip2) + } + -cmp [(str:split . $ip1)] [(str:split . $ip2)] +} + +fn comp6 {|ip1 ip2| + if (or (not (is-ipv6 $ip1)) (not (is-ipv6 $ip2))) { + fail (printf 'Not an IPv6: %s → %s' $ip1 $ip2) + } + -cmp [(str:split : (middle6 $ip1))] [(str:split : (middle6 $ip2))] +} + +fn comp {|ip1 ip2| + if (is-ipv4 $ip1) { + if (is-ipv4 $ip2) { + comp4 $ip1 $ip2 + } else { + put -1 + } + } elif (is-ipv6 $ip1) { + if (is-ipv4 $ip2) { + put 1 + } elif (is-ipv6 $ip2) { + comp6 $ip1 $ip2 + } else { + put -1 + } + } else { + fail (printf 'Not an IP: %s → %s' $ip1 $ip2) + } +} diff --git a/mods/list.elv b/mods/list.elv new file mode 100644 index 0000000..3d9052a --- /dev/null +++ b/mods/list.elv @@ -0,0 +1,178 @@ +use ./common +use ./num + +var -l~ = $common:to-list~ +var -c~ = $common:cexec~ +var '++~' = $'num:++~' +var is-neg~ = $num:is-neg~ + + +fn indexes {|&from=0 &step=1 @argv| + var c = (count (-l $@argv)) + var from = (-c (is-neg $from) (+ $from $c) $from) + var to = (-c (is-neg $step) -1 $c) + if (and (>= $from 0) (< $from $c)) { + range &step=$step $from $to + } +} + +fn loop {|&from=0 &step=1 @argv| + var l = (-l $@argv) + indexes &from=$from &step=$step $l | each {|i| + put [ $i $l[$i] ] + } +} + +fn foreach {|&from=0 &step=1 &end=$false cb @argv| + if (not $end) { + loop &from=$from &step=$step $@argv | each {|e| $cb $@e} + } else { + loop &from=$from &step=$step $@argv | each {|e| + if ($end $@e) { + break + } + $cb $@e + } + } +} + +fn pforeach {|&from=0 &step=1 cb @argv| + loop &from=$from &step=$step $@argv | peach {|e| $cb $@e} +} + +fn reach {|cb @argv| + foreach &from=-1 &step=-1 {|_ e| $cb $e} $@argv +} + +fn reverse {|@argv| + reach $put~ $@argv +} + +fn is-empty {|@argv| + num:is-zero (count (-l $@argv)) +} + +fn is-not-empty {|@argv| + not (is-empty $@argv) +} + +fn filter {|cb @argv| + each {|e| + -c ($cb $e) $e $nop~ + } (-l $@argv) +} + +fn filter-not {|cb @argv| + filter {|v| not ($cb $v)} $@argv +} + +fn contains {|cb @argv| + var cb2~ = (common:cond (eq (kind-of $cb) fn) $cb {|e| eq $cb $e}) + each {|e| + if (cb2 $e) { + put $true + return + } + } (-l $@argv) + put $false +} + +fn contains-not {|cb @argv| + not (contains $cb $@argv) +} + +fn search {|cb @argv| + foreach {|i e| + -c ($cb $i $e) [$i $e] $nop~ + } $@argv +} + +fn search-not {|cb @argv| + search {|i e| not ($cb $i $e) } $@argv +} + +fn exists {|cb @argv| + var r = $false + foreach {|i e| + if ($cb $i $e) { + set r = $true + break + } + } $@argv + put $r +} + +fn exists-not {|cb @argv| + not (exists $cb $@argv) +} + +fn search-first {|&from=$nil &reverse=$false cb @argv| + var step = (-c $reverse -1 1) + if (not $from) { + set from = (-c $reverse -1 0) + } + foreach &from=$from &step=$step {|i e| + if ($cb $i $e) { + put [$i $e] + break + } + } $@argv +} + +fn first {|&from=$nil &reverse=$false cb @argv| + search-first &from=$from &reverse=$reverse {|_ e| $cb $e} $@argv | each {|e| + put $e[1] + break + } +} + +fn reduce {|acc cb @argv| + each {|e| + set acc = ($cb $acc $e) + } (-l $@argv) + put $acc +} + +fn remove-duplicate {|@argv| + var done = [&] + each {|e| + if (not (has-key $done $e)) { + set done[$e] = $nil + put $e + } + } (-l $@argv) +} + +fn swap {|i j @argv| + var l = (-l $@argv) + var c = (count $l) + if (< $i 0) { + set i = (+ $i $c) + } + if (< $j 0) { + set j = (+ $j $c) + } + if (or (is-neg $i) (is-neg $j) (>= $i $c) (>= $j $c)) { + fail 'index out of range' + } + if (== $i $j) { + all $l + } else { + if (> $i $j) { + set i j = $j $i + } + take $i $l + put $l[$j] + all $l[(++ $i)..$j] + put $l[$i] + drop (++ $j) $l + } +} + +fn less {|cb| + put {|a b| < ($cb $a $b) 0 } +} + +fn sort {|cb @argv| + order &less-than=(less $cb) (-l $@argv) +} diff --git a/mods/map.elv b/mods/map.elv new file mode 100644 index 0000000..404967d --- /dev/null +++ b/mods/map.elv @@ -0,0 +1,81 @@ +use ./common +use ./list +use ./num + +fn is-empty {|m| + num:is-zero (count $m) +} + +fn values {|m| + keys $m | each {|k| put $m[$k]} +} + +fn pvalues {|m| + keys $m | peach {|k| put $m[$k]} +} + +fn value-of {|m k &default=$nil| + common:cexec (has-key $m $k) { put $m[$k] } { put $default } +} + +fn unzip {|m| + var keys values = [] [] + keys $m | each {|k| + set @keys = $@keys $k + set @values = $@values $m[$k] + } + put $keys $values +} + +fn zip {|keys values| + var ck cv = (count $keys) (count $values) + var c = (num:min [$ck $cv]) + var m = [&] + range $c | peach {|i| + put [&k=$keys[$i] &v=$values[$i]] + } | each {|e| + set m[$e[k]] = $e[v] + } + put $m +} + +fn to-map {|@argv| + var l = (list:-l $@argv) + zip [(list:indexes $l)] $l +} + +fn to-set {|@argv| + var m = [&] + each {|k| + set m[$k] = $nil + } (list:-l $@argv) + put $m +} + +fn mult-dissoc {|m @argv| + each {|k| + set m = (dissoc $m $k) + } $argv + put $m +} + +fn mult-assoc {|&replace=$true m @argv| + each {|e| + var k v = $@e + if (or $replace (not (has-key $m $k))) { + set m = (assoc $m $k $v) + } + } $argv + put $m +} + +fn add {|m k @values| + var values = (list:-l $values) + set m[$k] = (common:cexec (has-key $m $k) { put [(all $m[$k]) $@values ] } $values) + put $m +} + +fn foreach {|cb @m| + set m = (common:cexec (list:is-empty $m) $one~ { put $m[0] }) + keys $m | each {|k| put [$k $m[$k]]} | each {|e| $cb $@e} +} diff --git a/mods/num.elv b/mods/num.elv new file mode 100644 index 0000000..d562762 --- /dev/null +++ b/mods/num.elv @@ -0,0 +1,46 @@ +fn ++ {|n| + + $n 1 +} + +fn -- {|n| + - $n 1 +} + +fn neg {|n| + * $n -1 +} + +fn is-zero {|n| + == $n 0 +} + +fn is-neg {|n| + < $n 0 +} + +fn is-one {|n| + == $n 1 +} + +fn -minmax {|cb @numbers| + use ./common + var l = (common:to-list $@numbers) + if (is-zero (count $l)) { + return + } + var m = $l[0] + all $l[1..] | each {|n| + if ($cb $n $m) { + set m = $n + } + } + put $m +} + +fn min {|@numbers| + -minmax $'<~' $@numbers +} + +fn max {|@numbers| + -minmax $'>~' $@numbers +} diff --git a/mods/option.elv b/mods/option.elv new file mode 100644 index 0000000..c896b43 --- /dev/null +++ b/mods/option.elv @@ -0,0 +1,85 @@ +use re +use str +use ./common +use ./list +use ./map +use ./num + +fn is-aggregate {|option| + re:match '^\-\w{2,}$' $option +} + +fn is-short {|option| + re:match '^\-\w$' $option +} + +fn is-long {|option| + re:match '^\-\-\w+(=.*)?$' $option +} + +fn is-option {|option| + or (is-short $option) (is-long $option) (is-aggregate $option) +} + +fn split {|option| + if (is-aggregate $option) { + each {|e| put -$e} $option[1..] + } elif (is-long $option) { + var i = (str:index $option '=') + if (num:is-neg $i) { + put $option + } else { + put $option[..$i] $option[(num:++ $i)..] + } + } else { + put $option + } +} + +fn -j {|options| + var o = (each {|o| put $o[1..] } $options | str:join '') + if (not-eq $o '') { + put -$o + } +} + +fn join {|@options| + set options = (common:to-list $@options) + var cb = {|_ o| or (is-short $o) (is-aggregate $o) } + var i0 = 0 + list:search-not $cb $options | each {|e| + var i1 v = $@e + -j $options[$i0..$i1] + put $v + set i0 = (num:++ $i1) + } + -j $options[$i0..] +} + +fn expand {|@argv| + each $split~ (common:to-list $@argv) +} + +fn map {|@argv| + set @argv = (expand $@argv) + var result i c = [&] 0 (count $argv) + var last = (num:-- $c) + while (< $i $c) { + var e = $argv[$i] + if (is-option $e) { + var k v = $e [] + if (< $i $last) { + var j = (num:++ $i) + set e = $argv[$j] + if (not (is-option $e)) { + set i @v = $j $@v $e + } + } + set result = (map:add $result $k $v) + } else { + set result = (map:add $result '' [$e]) + } + set i = (num:++ $i) + } + put $result +} diff --git a/mods/pdf.elv b/mods/pdf.elv new file mode 100644 index 0000000..42b2937 --- /dev/null +++ b/mods/pdf.elv @@ -0,0 +1,420 @@ +use path +use re +use str +use ./common +use ./map + +fn is-pdf {|file| + eq (path:ext $file) '.pdf' +} + +fn exist {|file| + or (path:is-dir &follow-symlink=$true $file) (path:is-regular &follow-symlink=$true $file) +} + +fn not-exist {|file| + not (exist $file) +} + +fn is-pdf-exist {|file| + and (is-pdf $file) (exist $file) +} + +fn is-number {|v| + re:match '^\d+$' (to-string $v) +} + +fn is-page-selection {|v| + re:match '^(r?\d+|z)(-(r?\d+|z))?(,(r?\d+|z)(-(r?\d+|z))?)*(:(even|odd))?$' (to-string $v) +} + +fn is-rotate-selection {|v| + re:match '^(\+|-)?\d+(:(r?\d+|z)(-(r?\d+|z))?(,(r?\d+|z)(-(r?\d+|z))?)*(:(even|odd))?)?$' (to-string $v) +} + +fn is-selection {|v| + or (is-page-selection $v) (is-rotate-selection $v) +} + +fn is-object {|v| + re:match '^ \d+\ 0\ R$' $v +} + +fn version {|in| + var version = (head -n 1 $in) + set version @_ = (str:split "\r" $version) + str:trim-prefix $version '%PDF-' +} + +fn json {|in| + qpdf $in --json | from-json +} + +fn form {|in| + var json = (json $in) + put $json[acroform] +} + +fn encryption {|in| + var json = (json $in) + put $json[encrypt] +} + +fn objects {|in| + var json = (json $in) + put $json[objects] +} + +fn -format {|dict o| + var t = (kind-of $o) + if (eq $t string) { + if (has-key $dict $o) { + set o = $dict[$o] + } + } elif (eq $t list) { + set @o = (all $o | each {|e| + common:cexec (has-key $dict $e) { put $dict[$e] } $e + }) + } elif (eq $t map) { + map:foreach {|k v| + set o[$k] = (-format $dict $v) + } $o + } + put $o +} + + +fn -deep {|&dict=$nil objects @keys| + if (== (count $keys) 0) { + common:cexec (not-eq $dict $nil) { -format $dict $objects } $objects + put $true + return + } + if (not-eq $dict $nil) { + set objects = (-format $dict $objects) + } + var id @next = $@keys + var t = (kind-of $objects) + if (eq $t map) { + if (has-key $objects $id) { + -deep &dict=$dict $objects[$id] $@next + } + } elif (eq $t list) { + if (and (is-number $id) (< $id (count $objects))) { + -deep &dict=$dict $objects[$id] $@next + } + } else { + put $nil $false + } +} + +fn -object {|&extend=$false objects id| + var dict = (common:cond $extend $objects $nil) + var o _ = (-deep &dict=$dict $objects $id) + put $o +} + +fn deep {|&extend=$false in @keys| + var objects = (objects $in) + var dict = (common:cond $extend $objects $nil) + -deep &dict=$dict $dict $@keys +} + +fn contains {|in @keys| + var _ ok = (deep $in $@keys) + put $ok +} + +fn value {|&extend=$false in @keys| + var v _ = (deep &extend=$extend $in $@keys) + put $v +} + +fn object {|&extend=$false in key| + -object &extend=$extend (objects $in) $key +} + +fn -filter {|&extend=$false objects cond| + var out = [&] + map:foreach {|k o| + if ($cond $o) { + set out[$k] = $o + } + } $objects + put $out +} + +fn filter {|&extend=$false in cond| + -filter &extend=$extend (objects $in) $cond +} + +fn font-filter {|o| + and ^ + (eq (kind-of $o) map) ^ + (has-key $o /Type) ^ + (eq $o[/Type] /Font) +} + +fn fonts {|in| + var dict = (objects $in) + var fonts = (-filter $dict $font-filter~) + put $fonts | map:foreach {|id f| + map:foreach {|k v| + if (has-key $dict $v) { + set f[$k] = $dict[$v] + } + } $f + var fd = $f[/FontDescriptor] + map:foreach {|k v| + if (and (not-eq $k /FontFile2) (has-key $dict $v)) { + set fd[$k] = $dict[$v] + } + } $fd + set f[/FontDescriptor] = $fd + set f[id] = $id + put $f + } +} + +fn image-filter {|o| + and ^ + (eq (kind-of $o) map) ^ + (has-key $o /Subtype) ^ + (eq $o[/Subtype] /Image) +} + +fn images {|in| + var json = (json $in) + var dict = $json[objects] + all $json[pages] | each {|p| + var n = $p[pageposfrom1] + all $p[images] | each {|i| + var id = $i[object] + var img = (-object $dict $id) + map:foreach {|k v| + if (has-key $dict $v) { + set img[$k] = $dict[$v] + } + } $img + if (has-key $img /ColorSpace) { + var @cs = (all $img[/ColorSpace] | each {|v| + if (has-key $dict $v) { + put $dict[$v] + } else { + put $v + } + }) + set img[/ColorSpace] = $cs + } + if (has-key $img /DecodeParms) { + var dp = $img[/DecodeParms] + map:foreach {|k v| + if (has-key $dict $v) { + set dp[$k] = $dict[$v] + } + } $dp + set img[/DecodeParms] = $dp + } + set img[id] = $id + set img[page] = $n + set img[name] = $i[name] + put $img + } + } +} + +fn trailer {|&extend=$false in| + object &extend=$extend $in 'trailer' +} + +fn pages {|in| + var json = (json $in) + put $json[pages] +} + +fn page {|in nb| + put (pages $in)[(- $nb 1)] +} + +fn nb-pages {|in| + qpdf --show-npages $in +} + +fn attachments {|in| + var data = [] + var json = (json $in) + var attachments = $json[attachments] + if (> (count $attachments) 0) { + set @data = (qpdf --list-attachments $in | eawk {|_ f @_| + put $f + }) + } + put $data +} + +fn parse-selection {|@selection| + var out = [] + var in = $nil + each {|e| + if (is-pdf $e) { + if (not-exist $e) { + fail (printf '%s: le fichier n’existe pas' $e) + } + if (not-eq $in $nil) { + set @out = $@out $in + } + set in = [ + &file=$e + &rotate=$nil + &selections=[] + ] + continue + } + if (eq $in $nil) { + fail (printf '%s: pas de fichier avant la sélection' $e) + } + var r rc = $in[rotate] $nil + if (is-page-selection $e) { + set rc = $false + } elif (is-rotate-selection $e) { + set rc = $true + } else { + fail (printf '%s: paramètre invalide' $e) + } + if (not-eq $r $rc) { + if (not-eq $r $nil) { + set @out = $@out $in + set in[selections] = [] + } + set in[rotate] = $rc + } + set in[selections] = [ (all $in[selections]) $e ] + } $selection + if (not-eq $in $nil) { + set @out = $@out $in + } + put $out +} + +fn -t { + mktemp -q /tmp/qpdf_XXXXXXXXXX.pdf +} + +fn rotate {|&empty=$false &out=$nil &keep=$false in @rotate| + var @args = $in (common:cond (eq $out $nil) --replace-input $out) + if $empty { + set @args = $@args --empty + } + if $keep { + set @rotate = (each {|r| put --rotate=$r } $rotate) + qpdf $@args $@rotate + return + } + var tmp = [] + try { + var @tmp = (each {|r| + var t r @s = (-t) (str:split ':' $r) + set s = (str:join ':' $s) + if (eq $s '') { + qpdf $in $t --rotate=$r + } else { + qpdf $in $t --pages $in $s -- + qpdf $t --replace-input --rotate=$r + } + put $t + } $rotate) + qpdf $@args --pages $@tmp -- + } finally { + rm -f $@tmp + } +} + +fn -catarg {|selection| + var f r s = $selection[file] $selection[rotate] $selection[selections] + var out tmp = [] [] + if $r { + var t = (-t) + set @tmp = $t + set @out = $@out $t + rotate &out=$t $f $@s + } else { + set @out = $@out $f + if (> (count $s) 0) { + set @out = $@out (str:join ',' $s) + } + } + put $out $tmp +} + +fn cat {|&empty=$false &collate=$nil &out=$nil @selection| + var inputs = (parse-selection $@selection) + var pages tmp = [] [] + each {|in| + var a t = (-catarg $in) + set @pages = $@pages $@a + set @tmp = $@tmp $@t + } $inputs + try { + var in = $pages[0] + var @args = $in (common:cond (eq $out $nil) --replace-input $out) + if $empty { + set @args = $@args --empty + } + if $collate { + var c = --collate(common:cexec (is-number $collate) { put =$collate } '') + set @args = $@args $c + } + qpdf $@args --pages $@pages -- + } finally { + each {|t| + rm -f $t + } $tmp + } +} + +fn decrypt {|&out=$nil in @passwd| + var @args = $in (common:cond (eq $out $nil) --replace-input $out) --decrypt + if (> (count $passwd) 0) { + set @args = $@args $passwd[0] + } + qpdf $@args +} + +fn -l0 {|n size| + set n = (to-string $n) + var l = (- $size (count $n)) + str:join '' [ (repeat $l 0) $n ] +} + +fn split {|&empty=$false &size=1 in out| + if (not-exist $out) { + mkdir $out + } + var @args = (common:cexec $empty --empty $nop~) + qpdf $@args --split-pages=$size $in $out'/%d.pdf' +} + +fn uncat {|&empty=$false in out @selection| + if (not-exist $out) { + mkdir $out + } + var i = 1 + var n = (count $selection) + set n = (count (to-string $n)) + each {|s| + cat &empty=$empty &out=$out'/'(-l0 $i $n)'.pdf' $in $s + set i = (+ $i 1) + } $selection +} + +fn raw-stream {|in out id| + qpdf $in --show-object=$id --raw-stream-data > $out +} + +fn filtered-stream {|in out id| + qpdf $in --show-object=$id --filtered-stream-data --normalize-content=n --stream-data=uncompress --decode-level=all > $out +} + +fn attachment {|in out id| + qpdf $in --show-attachment=$id > $out +}