├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── Makefile ├── README.md ├── doc ├── concept-graphics.md ├── drafts │ ├── draft.md │ ├── flow-test.bash │ ├── help.md │ ├── operating-symbols.md │ ├── styles.txt │ └── utf8-symbols.txt ├── progress.md ├── quick-start-mod.md ├── spec │ ├── abbr.md │ ├── cmds.md │ ├── display.md │ ├── env.md │ ├── flow.md │ ├── help.md │ ├── hub.md │ ├── local-store.md │ ├── mod-interact.md │ ├── mod-meta.md │ ├── repo-tree.md │ ├── seq.md │ └── spec.md ├── usage │ ├── basic.md │ ├── cmds.md │ ├── env.md │ ├── flow.md │ ├── hub.md │ └── user-manual.md └── zen │ ├── why-abbrs.md │ ├── why-cli.md │ ├── why-hub.md │ ├── why-not-async.md │ ├── why-not-pipe.md │ ├── why-tags.md │ ├── why-ticat.md │ ├── why-tree.md │ ├── why-weird.md │ └── zen.md ├── go.mod ├── go.sum ├── install.sh └── pkg ├── cli └── display │ ├── abbrs.go │ ├── args.go │ ├── cmds.go │ ├── color.go │ ├── dep.go │ ├── env.go │ ├── env_ops.go │ ├── err.go │ ├── executor.go │ ├── flow.go │ ├── frame.go │ ├── help.go │ ├── parse_err.go │ ├── render.go │ ├── screen.go │ ├── suggest.go │ ├── tags.go │ ├── tip.go │ └── utils.go ├── core ├── execute │ ├── cmd_type.go │ ├── executor.go │ └── interactive.go ├── model │ ├── args.go │ ├── args_auto_map.go │ ├── argv.go │ ├── bg_tasks.go │ ├── blender.go │ ├── breakpoints.go │ ├── cli.go │ ├── cmd.go │ ├── cmds.go │ ├── dep.go │ ├── env.go │ ├── env_abbr.go │ ├── env_info.go │ ├── env_mapper.go │ ├── env_ops.go │ ├── env_val.go │ ├── errs.go │ ├── executed.go │ ├── executing.go │ ├── flow.go │ ├── forest.go │ ├── helps.go │ ├── parsing.go │ ├── proto.go │ ├── screen.go │ ├── session.go │ ├── sys_argv.go │ ├── template.go │ └── val.go └── parser │ ├── cmd.go │ ├── cmd_test.go │ ├── cmds.go │ ├── env.go │ ├── env_test.go │ ├── sequence.go │ └── sequence_test.go ├── main └── main.go ├── mods ├── builtin │ ├── api.go │ ├── bg_tasks.go │ ├── blender.go │ ├── builtin.go │ ├── dbg.go │ ├── display.go │ ├── dump_cmd.go │ ├── dump_cmds.go │ ├── dump_env.go │ ├── dump_flow.go │ ├── env.go │ ├── env_snapshot.go │ ├── flow.go │ ├── help.go │ ├── hub.go │ ├── interactive.go │ ├── join.go │ ├── misc.go │ ├── mod.go │ ├── sessions.go │ ├── system.go │ ├── tags.go │ ├── utils.go │ └── verb.go └── persist │ ├── flow_file │ └── flow_file.go │ ├── hub_meta │ ├── hub_meta.go │ ├── hub_repo.go │ └── repo.go │ ├── meta_file │ └── meta_file.go │ └── mod_meta │ └── mod_reg.go ├── ticat ├── consts.go └── ticat.go └── utils └── misc.go /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, built with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | # Building output 15 | bin 16 | 17 | # Temp directories or files 18 | *.swp 19 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log of ticat 2 | 3 | All notable changes to this project are documented here. 4 | 5 | ## [1.4.0] - 2022-09-20 6 | + Highlights 7 | + Support `args.auto` in meta, to auto generate args and arg2env for cmd (#192 #199 #201 #203 and more) 8 | + Support `pack-subflow` to totally hide suflow (#221) 9 | + New command `hook.exit/error/done` for hooking a flow with system events, to do result report or other jobs easily (#183) 10 | + Other New Features 11 | + Support `hide-in-sessions-last=true` `quiet=true` flag in meta files (#186 #188) 12 | + Support sys var `RANDOM` in meta file template (#218) 13 | + New command `env.rm.prefix`: batchly delete key-values by prefix (#213) 14 | + New command `session.running.desc.monitor` to monitor running status 15 | + New command `bg.after-main.*`: enable/disable auto wait for bg task when main thread ends (#205) 16 | + New command `display.sensitive` to display sensitive env values or args, which are hide by default (#210) 17 | + Display optimizations: fix lots of display bugs and some enhancements (#184 #185 #190 and more) 18 | + Important bug fixs 19 | + Fix bugs when use `desc` on an executing/executed session 20 | + Fix bugs when use breakpoints on a command with both flow and script 21 | 22 | ## [1.3.1] - 2022-06-15 23 | + New Features 24 | + Support combined meta file (#178) 25 | + Support macro definition in meta file (#176) 26 | + Env snapshot manage toolbox `env.snapshot.*` (#171) 27 | + Add break-point command: `break.here` 28 | + Default env key-values 29 | + New: `sys.paths.cache` for cache path (eg: download files) 30 | + New: `sys.paths.data.shared` for shared data (eg: repos could be used by more than one commands) 31 | 32 | ## [1.2.1] - 2022-04-01 33 | + New Features 34 | + Run command selftests in parallel mode (#125) 35 | + Add command branch `api`; add session id and host ip to env (#128) 36 | + support quiet-error flag in meta (#129) 37 | + Command `repeat`: run a command many times (#139) 38 | + (Disabled, too many bugs) A command set for dynamic changing flow during executing (#127) 39 | + Usability 40 | + not display sensitive(eg: password) key-value or args (#115) 41 | + add blender.forest-mode: reset env on each command, but not reset on their subflows (#118) 42 | + move command to global: `dbg.break.*`, `dbg.step`, `dbg.delay` (#126) 43 | + add commands: list sessions by type (#120) 44 | + Display enhance and bug fixes 45 | + Compatibility Changes 46 | + Change init repo from innerr/marsh.ticat to ticat-mods/marsh (#132) 47 | + Bug Fixs 48 | + Fix some bugs with tail-mode 49 | 50 | ## [1.2.0] - 2021-12-29 51 | 52 | + New Features 53 | + Support breakpoints and more executing control 54 | + Command `dbg.break.at ` [#97](https://github.com/innerr/ticat/pull/97) 55 | + Command `dbg.break.at.begin`, step in/out/over [#106](https://github.com/innerr/ticat/pull/106) 56 | + Interactive mode with history and completion [#108](https://github.com/innerr/ticat/pull/108) 57 | + Executed sessions management 58 | + Write logs for each command, shows them in executed session [#95](https://github.com/innerr/ticat/pull/95) 59 | + Retry an failed session from error point [#93](https://github.com/innerr/ticat/pull/93) 60 | + Tracing env changes in an executed session [#91](https://github.com/innerr/ticat/pull/91) 61 | + Display executed session details [#86](https://github.com/innerr/ticat/pull/86) 62 | + List, add, remove sessions [#83](https://github.com/innerr/ticat/pull/83) 63 | + Usability 64 | + Redesign command sets: `desc` `cmds` and more, for locating commands easier [#98](https://github.com/innerr/ticat/pull/98) 65 | + Remove tail-edit-mode usage info from all help-strings and hints, treat it as a hidden hack mode [#98](https://github.com/innerr/ticat/pull/98) 66 | + Compatibility Changes 67 | + Remove all command alias `*.--`(alias of `*.clear`) `*.+`(alias of `*.increase`) `*.-`(alias of `*.decrease`) 68 | + Rename command `dbg.echo` to `echo` 69 | + Rename command set `verbose` `quiet` to `display.verbose` `display.quiet` 70 | + Relocate repo local storage dir: `.../ => ...///` [#112](https://github.com/innerr/ticat/pull/112) 71 | + All capical abbrs of commands and args are removed 72 | 73 | ## [1.0.1] - 2021-11-29 74 | 75 | + New Features 76 | + Support background task by sys-arg `%delay` [#82](https://github.com/innerr/ticat/pull/82) 77 | + Support auto-timer in meta files `*.ticat` `*.tiflow` [#81](https://github.com/innerr/ticat/pull/81) 78 | + Add builtin commands `timer.begin` `timer.elapsed` for time recording [#77](https://github.com/innerr/ticat/pull/77) 79 | 80 | ## [1.0.0] - 2021-08-25 81 | 82 | + Init version 83 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: unit-test ticat 2 | .DEFAULT_GOAL := default 3 | 4 | REPO := github.com/innerr/ticat 5 | 6 | GOOS := $(if $(GOOS),$(GOOS),$(shell go env GOOS)) 7 | GOARCH := $(if $(GOARCH),$(GOARCH),$(shell go env GOARCH)) 8 | GOENV := GO111MODULE=on CGO_ENABLED=0 GOOS=$(GOOS) GOARCH=$(GOARCH) 9 | GO := $(GOENV) go 10 | GOBUILD := $(GO) build $(BUILD_FLAG) 11 | GOTEST := GO111MODULE=on CGO_ENABLED=1 go test -p 3 12 | SHELL := /usr/bin/env bash 13 | 14 | _COMMIT := $(shell git describe --no-match --always --dirty) 15 | _GITREF := $(shell git rev-parse --abbrev-ref HEAD) 16 | COMMIT := $(if $(COMMIT),$(COMMIT),$(_COMMIT)) 17 | GITREF := $(if $(GITREF),$(GITREF),$(_GITREF)) 18 | 19 | LDFLAGS := -w -s 20 | LDFLAGS += -X "$(REPO)/pkg/version.GitHash=$(COMMIT)" 21 | LDFLAGS += -X "$(REPO)/pkg/version.GitRef=$(GITREF)" 22 | LDFLAGS += $(EXTRA_LDFLAGS) 23 | 24 | FILES := $$(find . -name "*.go") 25 | 26 | default: unit-test ticat 27 | 28 | ticat: 29 | $(GOBUILD) -ldflags '$(LDFLAGS)' -o bin/ticat ./pkg/main 30 | 31 | unit-test: 32 | mkdir -p bin/cover 33 | $(GOTEST) ./pkg/... -covermode=count -coverprofile bin/cover/cov.unit-test.out 34 | -------------------------------------------------------------------------------- /doc/drafts/draft.md: -------------------------------------------------------------------------------- 1 | ## The '{...}' statement 2 | ``` 3 | # usage, target-name could be any of module-name or host-name or cluster-name 4 | $> ticat {[target-name] [target-name] [var=value]} sub-command 5 | 6 | # only show status of tikv and pd 7 | $> ticat {tikv pd} status 8 | ... 9 | 10 | # only show status of tikv on 172.16.5.4 11 | $> ticat {tikv [172.16.]5.4} status 12 | 13 | # show status of cluster except promethus on 172.16.5.4 14 | $> ticat {-prom[methus]} status 15 | 16 | # dry run mode 17 | $> ticat {debug.dry=on} tpcc/bench 18 | 19 | # restart all tikv 20 | $> ticat {tikv} fstop : up 21 | 22 | # restart all tikv+pd+tidb, then close all tidb 23 | $> ticat {tikv pd tidb} fstop : {tikv} up 24 | ``` 25 | 26 | ## Run a formal benchmark 27 | Run a formal benchmark on a 3 nodes cluster and create report when the optimization is done: 28 | ``` 29 | ## Acquire a server to run benchmark client, login to it 30 | $> ticat host.acquire core=16 : host.clone-ticat-to : host.login 31 | 32 | ## Acquire cluster hardware resource, deploy cluster, then save cluster info 33 | $> ticat cluster.deploy : env.save 34 | $> ticat hosts.acquire core=8 min-mem=16G cnt=3 : cluster.deploy : env.save 35 | 36 | ## Run the optimized new code on the new cluster, then release hardware resource 37 | $> ticat bb : ben.report : hosts.release 38 | ``` 39 | (actually there is a saved flow for formal benchmark, so we just run the command bellow) 40 | ``` 41 | $> ticat ben.scale 10 : ben.base 42 | ``` 43 | 44 | ## Investigate a performance issue 45 | (TODO: better example) 46 | Someone talk to us that our system has preformance jitter under a type of specific workload, 47 | So we fetch this workload and test it: 48 | ``` 49 | ## Fetch this workload 50 | $> ticat hub.add innerr/workload-x.demo.ticat 51 | 52 | ## Start local cluster and save info to env 53 | $> ticat cluster.local port=4000 : cluster.start : env.save 54 | 55 | ## Run benchmark and detect jitter 56 | $> ticat ben.x.scale 10 : ben.x.load : ben.x.run dur=10s : ben.scanner.jitter 57 | ``` 58 | -------------------------------------------------------------------------------- /doc/drafts/flow-test.bash: -------------------------------------------------------------------------------- 1 | echo "ticat dm : dbg.echo last =flow" 2 | ticat dm : dbg.echo last 3 | echo "-------------------" 4 | read 5 | echo "ticat dm ::dbg.echo first =tail-mode-flow" 6 | ticat dm ::dbg.echo first 7 | echo "-------------------" 8 | read 9 | echo "ticat dm ::dbg.echo first :- =recursive tail-mode-flow" 10 | ticat dm ::dbg.echo first :- 11 | echo "-------------------" 12 | read 13 | echo "ticat display.utf : e.ls =flow" 14 | ticat display.utf : e.ls 15 | echo "-------------------" 16 | read 17 | echo "ticat display.utf ::e.ls =tail-mode call (e.ls support it, and len(flow) == 2)" 18 | ticat display.utf ::e.ls 19 | echo "-------------------" 20 | read 21 | echo "ticat display.utf ::e.ls tip =mixed tail-mode-call" 22 | ticat display.utf ::e.ls tip 23 | echo "-------------------" 24 | read 25 | echo "ticat dm : display.utf ::e.ls sys =tail-mode-flow" 26 | ticat dm : display.utf ::e.ls sys 27 | ticat dm : display.utf ::e.ls sys:- 28 | echo "-------------------" 29 | read 30 | echo "ticat dm ::display.utf ::e.ls sys =recursive tail-mode-flow" 31 | ticat dm ::display.utf ::e.ls sys 32 | ticat dm ::display.utf ::e.ls sys :- 33 | echo "-------------------" 34 | read 35 | echo "ticat display.utf ::e.ls sys ::dbg.echo first =recursive tail-mode-flow" 36 | ticat display.utf ::e.ls sys ::dbg.echo first 37 | ticat display.utf ::e.ls sys ::dbg.echo first :- 38 | -------------------------------------------------------------------------------- /doc/drafts/help.md: -------------------------------------------------------------------------------- 1 | # Help and user guide 2 | 3 | First contact: 4 | ``` 5 | ## Show global help 6 | $> ticat 7 | 8 | ## Show global help 9 | $> ticat help 10 | $> ticat -help 11 | $> ticat --help 12 | $> ticat -h 13 | $> ticat -H 14 | ``` 15 | 16 | Global help 17 | ``` 18 | $> ticat hub 19 | $> ticat cmds 20 | $> ticat cmd:cmd:cmd: 21 | $> ticat {} 22 | ``` 23 | -------------------------------------------------------------------------------- /doc/drafts/operating-symbols.md: -------------------------------------------------------------------------------- 1 | # import operating symbols 2 | 3 | [-+] 4 | flow desc 5 | [=] 6 | tail cmd usage 7 | [==] cmd detail 8 | [/] find 9 | global find cmds 10 | [//] with detail 11 | [~] sub 12 | tail cmd branch find cmds 13 | [~~] with detail 14 | 15 | # candidate symbols 16 | 17 | [!] 18 | exe tail cmd 19 | [!!] exe tail cmd 20 | [^] 21 | head (same as tail) 22 | [@] tag 23 | tail cmd branch find tags 24 | [=] env 25 | tail cmd branch find env key 26 | 27 | [$] 28 | tail 29 | [$!] tail cmd usage 30 | [$!+] tail cmd detail 31 | 32 | [%] 33 | -------------------------------------------------------------------------------- /doc/drafts/utf8-symbols.txt: -------------------------------------------------------------------------------- 1 | 💡☀🌏 hint 2 | 3 | 🛑⛔ error 4 | 🚮❎✅ delete 5 | 6 | 7 | 🚧 warn 8 | 9 | 🎦 flow 10 | ♒ env 11 | 🔎 search result 12 | 13 | 🕕 14 | 🔔 15 | 16 | 🔍 17 | 🌚🌝🌞 18 | 🐈🐱🐾👉💩 19 | 😥🧐 20 | 21 | 〄⚪ 22 | ❌⚡ 23 | ‡↪↩ 24 | -------------------------------------------------------------------------------- /doc/progress.md: -------------------------------------------------------------------------------- 1 | # Roadmap and progress 2 | 3 | ## Progess 4 | ``` 5 | ***** Cli framework 6 | ***-- Command line parsing. TODO: char-escaping 7 | ----- Rewrite the shitty parser 8 | ****- Full context search 9 | ****- Full abbrs supporting. TODO: extra abbrs manage 10 | ****- Env framework. TODO: save or load from a tag 11 | ----- Command log and search 12 | ----- Command history and search 13 | ***** Mod framework 14 | ***** Env-ops framework 15 | ***** Env-ops dependencies checking 16 | ----- "one-of" statment: write-one-of(key1, key1) 17 | ***** Os-command dependencies: 18 | ----- Auto install? 19 | ***** Args supporting 20 | **--- Free number args 21 | ***** Mod-ticat interacting 22 | ***** Support mod types: 23 | ***** Builtin 24 | ****- Flags: power, quiet, priority. TODO: priority-level 25 | ***** File by ext: python, golang, ... 26 | ***** Executable file 27 | ***** Directory (include repo supporting) 28 | ----- Flow framework 29 | ***** Executor 30 | ***** Base executor 31 | ----- Middle re-enter 32 | ----- Intellegent interactive 33 | ----- Auto mocking 34 | ----- Background running 35 | ----- Concurrent running 36 | ***** Save, edit/remove flow 37 | ***** Help and abbrs 38 | ***** Executing ad-hot help 39 | ***** Combine mods' dependencies 40 | ***** Map env operations as command 41 | ***** Hub framework 42 | ***** Mod and flow sharing 43 | ***** Authority control (by git for now) 44 | ***** Command path or abbrs confliction 45 | ----- Module version manage 46 | ``` 47 | -------------------------------------------------------------------------------- /doc/spec/abbr.md: -------------------------------------------------------------------------------- 1 | # [Spec] Abbrs: commands, env-keys, flows 2 | 3 | ## Abbrs types 4 | There are 3 types of abbrs/aliases: 5 | * for command(command segments in path) 6 | * for env(key segments in path) 7 | * for args 8 | 9 | ## Display abbrs in a command 10 | Almost all displaying commands will show the abbrs, 11 | there are many abbrs info in the example bellow: 12 | * the command name `tree` with abbrs `t` and `T` 13 | * the command path: 14 | - `cmds` with abbrs/aliases: `cmd` `mod` `mods` `m` `M` 15 | - `tree` with abbrs `t` `T` 16 | * the args `path` with abbrs `p` `P` 17 | ``` 18 | $> ticat cmds.tree cmds.tree 19 | ... 20 | [tree|t|T] 21 | ... 22 | - full-abbrs: 23 | cmds|cmd|mod|mods|m|M.tree|t|T 24 | - args: 25 | path|p|P = '' 26 | ... 27 | ``` 28 | 29 | ## Abbrs borrowing 30 | When two command have a shared segment of path: 31 | ``` 32 | $> ticat dummy 33 | $> ticat dummy.power 34 | ``` 35 | 36 | If one defined abbrs for the shared segment and the other one didn't define: 37 | * `dummy`: no abbrs 38 | * `dummy.power`: full-abbrs is `dummy|dm.power` 39 | Then the other one cound also use the abbrs: 40 | ``` 41 | ## The command "dummy" borrowed the abbrs from "dm.power": 42 | $> ticat dm 43 | ``` 44 | 45 | ## Use abbrs in command 46 | ``` 47 | $> ticat env.tree 48 | $> ticat e.tree 49 | $> ticat e.t 50 | ``` 51 | 52 | ## Use abbrs in setting env key 53 | ``` 54 | $> ticat {display.width = 40} e.ls width 55 | display.width = 40 56 | $> ticat {disp.w = 60} e.ls width 57 | display.width = 60 58 | ``` 59 | 60 | ## Use abbrs in setting env key: with abbr-form command-prefix 61 | Env key could be a path composed by segments just as commands, 62 | so the env segments can form a tree. 63 | Although env tree and command tree are totally not related, 64 | we still let env tree borrow abbrs from commands for better usage: 65 | ``` 66 | $> ticat env{mykey=88}.ls mykey 67 | env.mykey = 88 68 | ## Command env has abbr "e" 69 | $> ticat e.ls 70 | ... 71 | ## Env key "env.mykey" borrowed abbrs from command "env" 72 | $> ticat e{mykey=66}.ls mykey 73 | env.mykey = 66 74 | ``` 75 | 76 | To show all env abbrs, including borrowed ones: 77 | ``` 78 | $> ticat env.abbrs 79 | ``` 80 | 81 | ## Use abbrs in the argv (not in arg names) 82 | (TODO: implement) 83 | ``` 84 | $> ticat flow.remove 85 | ``` 86 | -------------------------------------------------------------------------------- /doc/spec/cmds.md: -------------------------------------------------------------------------------- 1 | # [Spec] Command tree 2 | 3 | ## Overview of command branch `cmds` 4 | ``` 5 | $> ticat cmds.tree.simple cmds 6 | [cmds] 7 | 'display cmd info, sub tree cmds will not show' 8 | [tree] 9 | 'list builtin and loaded cmds' 10 | [simple] 11 | 'list builtin and loaded cmds, skeleton only' 12 | [list] 13 | 'list builtin and loaded cmds' 14 | [simple] 15 | 'list builtin and loaded cmds in lite style' 16 | ``` 17 | 18 | ## Execute a command 19 | ``` 20 | ## Execute a command under , with different ways to pass args 21 | $> ticat 22 | 23 | ## Examples: 24 | $> ticat dummy 25 | $> ticat sleep 26 | 27 | ## Execute a command with path (not under ) 28 | $> ticat 29 | 30 | ## Path is composed by and "." 31 | $> ticat .. 32 | 33 | ## Examples: 34 | $> ticat dummy.power 35 | $> ticat dummy.quiet 36 | 37 | ## Execute a command with path, in different ways to pass args 38 | $> ticat 39 | $> ticat = = 40 | $> ticat {= =} 41 | 42 | ## Examples: 43 | $> ticat dbg.echo hello 44 | $> ticat dbg.echo msg=hello 45 | $> ticat dbg.echo {M=hello} 46 | 47 | ## Quoting is useful 48 | $> ticat dbg.echo "hello world" 49 | 50 | ## Spaces(\s\t) are allowed 51 | $> ticat dbg.echo m = hello 52 | $> ticat dbg.echo {M = hello} 53 | ``` 54 | 55 | ## List commands 56 | ``` 57 | ## Display all in tree format 58 | $> ticat cmds.tree 59 | 60 | ## Display all in tree format, only names 61 | $> ticat cmds.tree.simple 62 | 63 | ## Display all in list format 64 | $> ticat cmds.list 65 | ``` 66 | 67 | ## Find commands 68 | ``` 69 | ## Display a specific path of the command tree 70 | $> ticat cmds.tree 71 | 72 | ## Examples: 73 | $> ticat cmds.tree dbg 74 | $> ticat cmds.tree dbg.echo 75 | 76 | # Find commands with any info about "echo" 77 | $> ticat cmds.ls 78 | $> ticat help 79 | $> ticat find 80 | `` 81 | -------------------------------------------------------------------------------- /doc/spec/display.md: -------------------------------------------------------------------------------- 1 | # [Spec] Display: styles and verb level 2 | 3 | ## The display flags 4 | There is a set of env key defining how much info will be showed to users: 5 | ``` 6 | $> ticat env.ls display 7 | display.bootstrap = false 8 | display.env = true 9 | display.env.default = false 10 | display.env.display = false 11 | display.env.layer = false 12 | display.env.sys = false 13 | display.env.sys.paths = false 14 | display.executor = true 15 | display.executor.end = false 16 | display.max-cmd-cnt = 7 17 | display.meow = false 18 | display.mod.quiet = false 19 | display.mod.realname = true 20 | display.one-cmd = false 21 | display.style = ascii 22 | display.utf8 = true 23 | ``` 24 | 25 | We could change this env manually, or use some builtin commands for convenient. 26 | 27 | ## The display style of the sequence box 28 | The value of "display.style" could be "ascii" or "utf" or "slash" or "no-corner", 29 | We could choose one as we like. 30 | 31 | If "display.style" is "utf", 32 | "display.utf8" need to be "true" to display frames in utf8 charset. 33 | 34 | ``` 35 | $> ticat {dis.style=ascii}:dummy:dummy 36 | +-------------------+ 37 | | stack-level: [1] | 05-24 04:05:33 38 | +-------------------+----------------------------------------------------------+ 39 | | >> dummy | 40 | | dummy | 41 | +------------------------------------------------------------------------------+ 42 | dummy cmd here 43 | +-------------------+ 44 | | stack-level: [1] | 05-24 04:05:33 45 | +-------------------+----------------------------------------------------------+ 46 | | dummy | 47 | | >> dummy | 48 | +------------------------------------------------------------------------------+ 49 | dummy cmd here 50 | 51 | $> ticat {dis.style=utf}:dummy:dummy 52 | ┌───────────────────┐ 53 | │ stack-level: [1] │ 05-24 04:05:03 54 | ├───────────────────┴──────────────────────────────────────────────────────────┐ 55 | │ >> dummy │ 56 | │ dummy │ 57 | └──────────────────────────────────────────────────────────────────────────────┘ 58 | dummy cmd here 59 | ┌───────────────────┐ 60 | │ stack-level: [1] │ 05-24 04:05:03 61 | ├───────────────────┴──────────────────────────────────────────────────────────┐ 62 | │ dummy │ 63 | │ >> dummy │ 64 | └──────────────────────────────────────────────────────────────────────────────┘ 65 | dummy cmd here 66 | ``` 67 | 68 | ## The verb commands 69 | The overview of verb commands: 70 | ``` 71 | $> ticat verb:- 72 | [verbose] 73 | 'change into verbose mode' 74 | [verbose.default] 75 | 'set to default verbose mode' 76 | [verbose.increase] 77 | 'increase verbose' 78 | [verbose.decrease] 79 | 'decrease verbose' 80 | 81 | $> ticat q:- 82 | [quiet|q|Q] 83 | 'change into quiet mode' 84 | - cmd-type: 85 | normal (quiet) 86 | - from: 87 | builtin 88 | ``` 89 | 90 | The commands "verb" and "quiet" is the fast way to display minimum/maximum info: 91 | ``` 92 | # Display all info when executing 93 | $> ticat verb : dummy : dummy 94 | $> ticat v : dummy : dummy 95 | 96 | # Not display any info when executing 97 | $> ticat quiet : dummy : dummy 98 | $> ticat q : dummy : dummy 99 | ``` 100 | 101 | The commands under "verb"(alias: v) is convenient to ajust the amount: 102 | ``` 103 | $> ticat verb.increase : dummy : dummy 104 | $> ticat v.+ : dummy : dummy 105 | $> ticat v.+ 2 : dummy : dummy 106 | $> ticat quiet : verb.default : dummy : dummy 107 | ``` 108 | 109 | The detail of verb commands: 110 | ``` 111 | $> ticat cmds.tree.simple verb 112 | [verbose|verb|v|V] 113 | 'change into verbose mode' 114 | - cmd-type: 115 | normal (quiet) 116 | - from: 117 | builtin 118 | [default|def|d|D] 119 | 'set to default verbose mode' 120 | - full-cmd: 121 | verbose.default 122 | - full-abbrs: 123 | verbose|verb|v|V.default|def|d|D 124 | - cmd-type: 125 | normal (quiet) 126 | - from: 127 | builtin 128 | [increase|inc|v+|+] 129 | 'increase verbose' 130 | - full-cmd: 131 | verbose.increase 132 | - full-abbrs: 133 | verbose|verb|v|V.increase|inc|v+|+ 134 | - args: 135 | volume|vol|v|V = 1 136 | - cmd-type: 137 | normal (quiet) 138 | - from: 139 | builtin 140 | [decrease|dec|v-|-] 141 | 'decrease verbose' 142 | - full-cmd: 143 | verbose.decrease 144 | - full-abbrs: 145 | verbose|verb|v|V.decrease|dec|v-|- 146 | - args: 147 | volume|vol|v|V = 1 148 | - cmd-type: 149 | normal (quiet) 150 | - from: 151 | builtin 152 | 153 | $> ticat cmd quiet 154 | [quiet|q|Q] 155 | 'change into quiet mode' 156 | - cmd-type: 157 | normal (quiet) 158 | ``` 159 | -------------------------------------------------------------------------------- /doc/spec/env.md: -------------------------------------------------------------------------------- 1 | # [Spec] Env get/set/save 2 | 3 | ## Overview 4 | ``` 5 | $> ticat cmds.tree.simple env 6 | [env] 7 | 'list env values in flatten format' 8 | [tree] 9 | 'list all env layers and KVs in tree format' 10 | [abbrs] 11 | 'list env tree and abbrs' 12 | [list] 13 | 'list env values in flatten format' 14 | [save] 15 | 'save session env changes to local' 16 | [remove-and-save] 17 | 'remove specific env KV and save changes to local' 18 | [reset-and-save] 19 | 'reset all local saved env KVs' 20 | ``` 21 | 22 | ## Display/find env values 23 | ``` 24 | $> ticat env.list 25 | $> ticat env.list 26 | ``` 27 | 28 | ## Define(set) env key-values 29 | ``` 30 | ## Set value 31 | $> ticat {key=value} 32 | 33 | ## Examples: 34 | $> ticat {display.width=40} 35 | $> ticat {display.width=40} : env.list 36 | 37 | ## Set value between segments of a command: 38 | $> ticat {key=value} 39 | 40 | ## Set value between segments of a command: 41 | $> ticat {key=value}. 42 | 43 | ## Example: 44 | $> ticat env{mykey=666}.ls mykey 45 | env.mykey = 666 46 | ``` 47 | 48 | Extra space chars (space and tab) will be ignore: 49 | ``` 50 | $> ticat {display.width = 40} 51 | ``` 52 | 53 | ## Save env key-values 54 | The command "env.save" will persist any changes in the env to disk, 55 | then use the values in later executing: 56 | ``` 57 | ## Display the default value of "display.width" 58 | $> ticat env.list width 59 | display.width = 80 60 | 61 | ## Change the value but not save 62 | $> ticat {display.width=40} env.list width 63 | display.width = 40 64 | 65 | ## Save the value 66 | $> ticat {display.width=60} env.save 67 | display.width = 60 68 | 69 | ## Show the changed and saved value 70 | $> ticat env.list width 71 | display.width = 60 72 | ``` 73 | 74 | ## Sessions 75 | Each time ticat is run, there is a session. 76 | When the execution finishes, the session ends. 77 | ``` 78 | $> ticat (session start) : : : (session end) 79 | ``` 80 | 81 | In a session, there is an env instance, 82 | all key-value changes between "start" and "end" will be in this env. 83 | When the session ends, all changed will be lost if "env.save" is not called. 84 | 85 | If a command call other ticat commands, 86 | then they all in the same session (share the same key-value set): 87 | ``` 88 | ## In this sequence, the value of "display.width" for also is "66" 89 | $> ticat {display.width=66} : : 90 | ``` 91 | 92 | ## Env layers 93 | Env has multi-layers, when getting a value, will find in the first layer, 94 | if the key is found then return the value. 95 | If the key is not found then looking in the next layer. 96 | ``` 97 | command layer - the first layer 98 | session layer 99 | persisted layer 100 | default layer - the last layer 101 | ``` 102 | 103 | Meanings of each layer: 104 | ``` 105 | command layer - the key-values only for this current command in the sequence 106 | session layer - the key-values for the whole sequence 107 | persisted layer - the key-values from env.saved, for the whole sequence 108 | default layer - the default values, hard-coded 109 | ``` 110 | 111 | Display each layer: 112 | ``` 113 | ## Display all: 114 | $> ticat env.tree 115 | 116 | ## Display in sequence execution: 117 | $> ticat {display.layer=true} dummy : dummy 118 | $> ticat {display.layer=true} dummy : {example-key=its-command-layer} dummy 119 | 120 | ## Save the display flay: 121 | $> ticat {display.layer=true} env.save 122 | ## Then every sequence executions will display layers: 123 | $> ticat dummy: dummy 124 | ``` 125 | 126 | ## Difference of the command layer and the session layer 127 | If the key-values settings has ":" in front of them, they are in command layer. 128 | ``` 129 | ## This is a command layer key-value, will only affect the 2nd dummy command: 130 | $> ticat dummy : {example-key=its-command-layer} dummy 131 | 132 | ## This is a session layer key-value, will only affect the 2nd dummy command: 133 | $> ticat {example-key=its-session-layer} dummy : dummy 134 | 135 | ## Demonstrate: 136 | $> ticat {display.width=40} dummy : dummy 137 | $> ticat dummy : {display.width=40} dummy 138 | ``` 139 | 140 | If a command changes env in it's code (not in cli), the changes will be in the session layer: 141 | ``` 142 | ## The "display.width" value for will be a changed value 143 | $> ticat : 144 | ``` 145 | 146 | ## The saved env file 147 | The saved file dir is defined by env key "sys.paths.data", 148 | the file name is defined by env key "strs.env-file-name". 149 | 150 | The format is multi lines, 151 | each line is a key-value pair seperated by a string defined by env key "strs.env-kv-sep"(default: =). 152 | -------------------------------------------------------------------------------- /doc/spec/flow.md: -------------------------------------------------------------------------------- 1 | # [Spec] Flow save/edit 2 | 3 | ## Save sequecen to a flow / use a saved flow 4 | ``` 5 | ## Save 6 | $> ticat : : flow.save 7 | ## Call 8 | $> ticat 9 | 10 | ## Examples: 11 | $> ticat dummy : dummy : flow.save dummy.2 12 | $> ticat dummy.2 13 | $> ticat sleep 1s : dummy.2 : echo hello 14 | ``` 15 | 16 | ## List all manually saved flows 17 | The flows from repos added by "hub.add" or "hub.add.local" will be not listed: 18 | ``` 19 | $> ticat flow.list 20 | $> ticat f.ls 21 | 22 | ## Command `flow` == `flow.list` 23 | $> ticat f 24 | ``` 25 | 26 | ## Add more info to the saved flow 27 | ``` 28 | ## Add help 29 | $> ticat flow.help 30 | 31 | ## Examples: 32 | $> ticat flow.help dummy.2 "just a simple test of flow" 33 | 34 | ## Add abbrs 35 | (TODO: implement, for now we could manually edit the flow file) 36 | ``` 37 | 38 | ## The saved flow files 39 | The saved file dir is defined by env key "sys.paths.flows", 40 | the file name is `` plus suffix `.flow.ticat`. 41 | 42 | The file format: 43 | * Share the same format with `mod meta` file except. 44 | * Use key `flow` instead of `cmd` in meta file, the value is the content of flow. 45 | * Template format `[[env-key]]` can be used in the content of flow, will be rendered into env value when executing. 46 | 47 | ## Flow commands overview 48 | ``` 49 | ## Overview 50 | $> ticat cmds.tree.simple flow 51 | [flow] 52 | 'list local saved but unlinked (to any repo) flows' 53 | [save] 54 | 'save current cmds as a flow' 55 | [set-help-str] 56 | 'set help str to a saved flow' 57 | [remove] 58 | 'remove a saved flow' 59 | [list-local] 60 | 'list local saved but unlinked (to any repo) flows' 61 | [load] 62 | 'load flows from local dir' 63 | [clear] 64 | 'remove all flows saved in local' 65 | [move-flows-to-dir] 66 | 'move all saved flows to a local dir (could be a git repo). 67 | auto move: 68 | * if one (and only one) local dir exists in hub 69 | * and the arg "path" is empty 70 | then flows will move to that dir' 71 | ``` 72 | 73 | Usage examples: 74 | ``` 75 | ## Move saved flows to a local dir 76 | $> ticat hub.move-flows-to-dir 77 | $> ticat hub.move 78 | 79 | ## Remove a saved flow 80 | $> ticat flow.rm 81 | 82 | ## Remove all saved flows 83 | $> ticat flow.clear 84 | ``` 85 | -------------------------------------------------------------------------------- /doc/spec/help.md: -------------------------------------------------------------------------------- 1 | # [Spec] Help 2 | 3 | ## The helping commands (commands provide info) 4 | ``` 5 | ## Sequence info 6 | $> ticat : : desc 7 | 8 | ## Hub info 9 | $> ticat hub.list 10 | 11 | ## Flow info 12 | $> ticat flow.list 13 | 14 | ## Command info 15 | $> ticat cmds.list 16 | $> ticat cmds.tree 17 | $> ticat cmds.tree.simple 18 | 19 | ## Env info 20 | $> ticat env.list 21 | $> ticat env.tree 22 | 23 | ## Abbrs info 24 | $> ticat env.abbrs 25 | 26 | ## Find command, env 27 | ## ..and abbrs(TODO: implement) 28 | $> ticat find ... 29 | ``` 30 | 31 | (TODO: all things bellow are removed, too complicated) 32 | ## The commands `+` and `-` 33 | These two are shortcuts, could display info base on the situation: 34 | ``` 35 | $> ticat cmd - 36 | [less|-] 37 | 'display brief info base on: 38 | * if in a sequence having 39 | * more than 1 other commands: show the sequence execution. 40 | * only 1 other command and 41 | * has no args and the other command is 42 | * a flow: show the flow execution. 43 | * not a flow: show the command or the branch info. 44 | * has args: find commands under the branch of the other command. 45 | * if not in a sequence and 46 | * has args: do global search. 47 | * has no args: show global help.' 48 | - args: 49 | 1st-str|find-str = '' 50 | 2nd-str = '' 51 | 3rh-str = '' 52 | 4th-str = '' 53 | - cmd-type: 54 | power (quiet) (priority) 55 | - from: 56 | builtin 57 | 58 | $> ticat cmd + 59 | [more|+] 60 | 'display rich info base on: 61 | * if in a sequence having 62 | * more than 1 other commands: show the sequence execution. 63 | * only 1 other command and 64 | * has no args and the other command is 65 | * a flow: show the flow execution. 66 | * not a flow: show the command or the branch info. 67 | * has args: find commands under the branch of the other command. 68 | * if not in a sequence and 69 | * has args: do global search. 70 | * has no args: show global help.' 71 | - args: 72 | 1st-str|find-str = '' 73 | 2nd-str = '' 74 | 3rh-str = '' 75 | 4th-str = '' 76 | - cmd-type: 77 | power (quiet) (priority) 78 | - from: 79 | builtin 80 | ``` 81 | 82 | Examples: 83 | ``` 84 | ## Global help, tips for how to use ticat 85 | $> ticat + 86 | $> ticat - 87 | 88 | ## Use them to find commands 89 | $> ticat - ... 90 | $> ticat + ... 91 | 92 | ## Other usages: 93 | (TODO: doc, most are already in usage doc) 94 | ``` 95 | -------------------------------------------------------------------------------- /doc/spec/hub.md: -------------------------------------------------------------------------------- 1 | # [Spec] Hub: list/add/add-local/disable/enable/purge 2 | 3 | ## Overview 4 | ``` 5 | $> ticat cmds.tree.simple hub 6 | [hub] 7 | 'list dir and repo info in hub' 8 | [clear] 9 | 'remove all repos from hub' 10 | [init] 11 | 'add and pull basic hub-repo to local' 12 | [add-and-update] 13 | 'add and pull a git address to hub' 14 | [local-dir] 15 | 'add a local dir (could be a git repo) to hub' 16 | [list] 17 | 'list dir and repo info in hub' 18 | [purge] 19 | 'remove an inactive repo from hub' 20 | [purge-all-inactive] 21 | 'remove all inactive repos from hub' 22 | [update-all] 23 | 'update all repos and mods defined in hub' 24 | [enable-repo] 25 | 'enable matched git repos in hub' 26 | [disable-repo] 27 | 'disable matched git repos in hub' 28 | [move-flows-to-dir] 29 | 'move all saved flows to a local dir (could be a git repo). 30 | auto move: 31 | * if one (and only one) local dir exists in hub 32 | * and the arg "path" is empty 33 | then flows will move to that dir' 34 | ``` 35 | 36 | ## List dirs in hub 37 | Hub is a set of local local dirs which **ticat** knows 38 | ``` 39 | $> ticat hub.list 40 | $> ticat hub.list 41 | 42 | ## Command `hub` == `hub.list` 43 | $> ticat hub 44 | 45 | ## Example: 46 | $> ticat hub.list examples 47 | ``` 48 | 49 | ## Add/update git addresses 50 | ``` 51 | ## Add(link) git address 52 | $> ticat hub.add 53 | $> ticat hub.add 54 | 55 | ## Example: 56 | $> ticat hub.add innerr/tidb.ticat 57 | 58 | ## Update all linked git repos 59 | $> ticat hub.update 60 | 61 | ## Add builtin (default) address 62 | $> ticat hub.init 63 | ``` 64 | 65 | ## Add local dirs 66 | ``` 67 | $> ticat hub.add.local path= 68 | 69 | ## Example: 70 | $> ticat hub.add.local path=./mymods 71 | ``` 72 | 73 | ## Disable repos or dirs, the modules in disabled repos or dirs can't be loaded 74 | ``` 75 | $> ticat hub.disable 76 | $> ticat hub.enable 77 | ``` 78 | 79 | ## Unlink repos/dirs from ticat 80 | 81 | Purge will delete all content of linked repos, 82 | but only remove meta info from **ticat** for local dirs. 83 | Only disabled ones can be purged 84 | ``` 85 | $> ticat hub.purge 86 | $> ticat hub.purge.all 87 | ``` 88 | 89 | ## The stored repo/file 90 | The saved dir is defined by env key "sys.paths.hub", 91 | All git cloned repos will be here. 92 | 93 | There is a repo list file, its name is defined by env key "strs.hub-file-name". 94 | The format is multi lines, each line has fields `git-address` `add-reason` `dir-path` `help-str` `on-or-off` seperated by "\t". 95 | -------------------------------------------------------------------------------- /doc/spec/local-store.md: -------------------------------------------------------------------------------- 1 | # [Spec] Ticat local store 2 | 3 | ## The local store dir 4 | **Ticat**'s store dir is defined by env key "sys.paths.data", 5 | the default value is the **ticat** binary path plus suffix ".data". 6 | 7 | Users can pass new dir to **ticat** to modify the store dir: 8 | (TODO: implement) 9 | ``` 10 | $> ticat {sys.paths.data=./mydir} : ... 11 | ``` 12 | 13 | ## The "flows", "hub" and "sessions" dir 14 | The flows/hub/sessions store dirs are all under store dir: 15 | * "sys.paths.data"/flows 16 | * "sys.paths.data"/hub 17 | * "sys.paths.data"/sessions 18 | 19 | There are env keys to change these dirs: 20 | * "sys.paths.flows" 21 | * "sys.paths.hub" 22 | * "sys.paths.sessions" 23 | (TODO: implement, now they are all only under store dir) 24 | -------------------------------------------------------------------------------- /doc/spec/mod-interact.md: -------------------------------------------------------------------------------- 1 | # [Spec] Module: env interaction with ticat 2 | 3 | ## Receive env 4 | A module is en executable file, 5 | the first arg "arg-0" will be it's own path or name as normal cases. 6 | 7 | The second arg "arg-1" will be an dir stored the ticat session info, 8 | The file "arg-1"/env is a file contains all the env values. 9 | The format is multi lines, each line is a key-value pair, seperated by "\t". 10 | 11 | The rest of args will be the normal args defined by ".ticat", in order. 12 | 13 | ## Change env 14 | When a module want to change env, it could simply append key-value to the env file, 15 | in the same "key \t value" format. 16 | 17 | ## Call other ticat modules 18 | A env key "sys.paths.ticat" defined the binary path of **ticat**, 19 | so it's easy to call other modules. 20 | 21 | We need to notify ticat it's in a same session, 22 | so all the modules could use the same env. 23 | That's how we should do to call other modules inside a module: 24 | ``` 25 | $> ticat {session=} 26 | $> ticat {session=} : : 27 | ``` 28 | -------------------------------------------------------------------------------- /doc/spec/mod-meta.md: -------------------------------------------------------------------------------- 1 | # [Spec] Module: meta file 2 | 3 | ## Ticat will load modules from hub, aka, local dirs 4 | We could add local dir to **ticat** by: 5 | ``` 6 | $> ticat hub.add.local path= 7 | ``` 8 | Or add a git repo, **ticat** will do "git clone" to local dir: 9 | ``` 10 | $> ticat hub.add.local path= 11 | ``` 12 | 13 | ## Dir struct and command tree 14 | **Ticat** will search any file with ext name ".ticat" in a local dir in hub. 15 | For each "file-or-dir-path.ticat" file, will try to register it to the command tree. 16 | 17 | The position in the tree will depends on the relative path to the repo-root, 18 | the ext(s) will be ignored in registering. 19 | 20 | For a dir "dir-path" with "dir-path.ticat", the ".ticat" file format is: 21 | ``` 22 | help = 23 | abbrs = ||... 24 | cmd = 25 | ``` 26 | If the values are quoted, the "'\"" will be removed. 27 | Key "abbrs" and "abbr" are equal. 28 | If "cmd" is not provided, a empty command will be registered. 29 | 30 | Multi-line value is supported with "\\"(single \) as line breaker. 31 | 32 | A dir without meta file "dir-path.ticat" will not be searched, 33 | so modules inside it will not be registered. 34 | 35 | For a file "file-path" with "file-path.ticat", the ".ticat" file format is: 36 | ``` 37 | help = 38 | abbrs = ||... 39 | 40 | [args] 41 | arg-1|| = 42 | arg-2 = 43 | ... 44 | 45 | [env] 46 | env-key-1 = 47 | env-key-2 = : : ... 48 | ... 49 | 50 | [val2env] 51 | env-key-1 = 52 | env-key-2 = 53 | ... 54 | 55 | [arg2env] 56 | env-key-1 = arg-1 57 | ... 58 | 59 | [dep] 60 | os-cmd-1 = 61 | os-cmd-2 = 62 | ... 63 | ``` 64 | The "help" and "abbrs" are the same with dir type of registering. 65 | The `[dep]` section defines what os-command will be called in the command's code. 66 | 67 | The `[args]` section defines the command's args with order. 68 | Abbrs definition are allowed, seperate them with "|". 69 | 70 | The `[env]` section defines which keys will read or write in the command's code. 71 | "env-op" value could be: "read", "write", "may-read", "may-write". 72 | The sequence of "env-op" could be one or more value with orders, seperated by ":". 73 | Abbrs definition are also allowed in every path segment of the keys. 74 | 75 | The `[val2env]` section defines keys will be written values automatically. 76 | This is convenient for writing "on|off" switch commands. 77 | 78 | The `[arg2env]` section defines keys will be written automatically with specify arg's value. 79 | This is convenient for deliver commands with args any without any env manipulating, 80 | so non-ticat-users could use them easily. 81 | 82 | ## Example 83 | Dir struct: 84 | ``` 85 | 86 | ├── README.md 87 | ├── tidb (dir) 88 | │   ├── stop.bash 89 | │   └── stop.bash.ticat 90 | ├── tidb.ticat 91 | └── misc (dir) 92 |     ├── run.bash 93 |    └── run.bash.ticat 94 | ``` 95 | 96 | File "tidb.ticat": 97 | ``` 98 | help = simple test toolbox for tidb 99 | abbrs = ti|db 100 | ``` 101 | 102 | File "stop.bash.ticat": 103 | ``` 104 | help = stop a tidb cluster 105 | abbr = down|dn 106 | 107 | [args] 108 | force|f = true 109 | 110 | [env] 111 | cluster|c.name|n = read:write 112 | ``` 113 | 114 | What will happend: 115 | * "run.bash" won't be registered, because "misc" with not "misc.ticat" 116 | * "tidb" will be registered, because of "tidb.ticat" 117 | 118 | Usage: 119 | ``` 120 | ## This will do nothing 121 | $> ticat tidb 122 | ## This will do nothing either, we use abbr to call "tidb" 123 | $> ticat db 124 | 125 | ## Display command info of "tidb.stop", the ext name ".bash" is ignored 126 | $> ticat cmds.tree tidb.stop 127 | $> ticat cmds.tree db.down 128 | $> ticat m.t db.dn 129 | 130 | ## Error: "tidb.stop" will read a key from env without any provider: 131 | $> ticat tidb.stop 132 | $> ticat db.stop 133 | 134 | ## Proper ways to call "tidb.stop", some use abbrs in the key path: 135 | $> ticat {cluster.name = test} db.stop 136 | $> ticat {c.n = test} db.stop 137 | $> ticat : db.stop 138 | 139 | ## Use saved env to call "tidb.stop": 140 | $> ticat {cluster.name = test} env.save 141 | ... 142 | $> ticat db.stop 143 | ``` 144 | 145 | ## Register conflictions 146 | When more than one command register to a same command path, 147 | or more than one abbrs for a command path segment, 148 | **ticat** will display conflicting errors. 149 | 150 | ## TODO 151 | ``` 152 | [arg2env] (only execute when env key not exists, and arg is not empty) 153 | 154 | [val2env] (always execute, and always before arg2env and comand-exe) 155 | 156 | [flow/][/flow] multiply flow template 157 | ``` 158 | -------------------------------------------------------------------------------- /doc/spec/repo-tree.md: -------------------------------------------------------------------------------- 1 | # [Spec] Git repos in ticat 2 | 3 | ## A git repo in ticat 4 | Any repo could added to **ticat** by: 5 | ``` 6 | $> ticat hub.add 7 | ``` 8 | 9 | A repo could provide 3 types of things to **ticat**: 10 | * modules, will be register to command tree by the relative path in the repo 11 | * flows, could be in any dir in the repo, will be register to command tree by it's file name 12 | * sub-repo list, **ticat** will add sub-repos to hub automatically 13 | 14 | ## Repo tree 15 | A repo could provide sub-repos, sub-repo could provide sub-sub-repos, so they could form a repo tree. 16 | 17 | This is useful for developer to organize features. 18 | And also useful for end-users to choose what to add to hub (eg, only add a branch). 19 | 20 | ## Sub-repo list defining format 21 | A repo could define it's sub-repos in a specific file, 22 | the file name is defined by env value "strs.repos-file-name", default value is "README.md" 23 | 24 | In this file, every line after a special mark `[ticat.hub]` will consider as one repo, 25 | The line format will be: `* [git-address](could-be-anything): help string of this repo` 26 | 27 | **ticat** will also try to use part(the part after ":") of the first line(first line has a ":") in "README.me" as help string, 28 | if no other place provides help string. 29 | -------------------------------------------------------------------------------- /doc/spec/seq.md: -------------------------------------------------------------------------------- 1 | # [Spec] Ticat command sequences 2 | 3 | (TODO: tail-mode flow, and tail-mode call) 4 | 5 | ## Execute a sequence of command 6 | A command sequence will execute commands one by one, 7 | the latter one won't start untill the previous one finishes. 8 | Commands in a sequence are seperated by ":". 9 | ``` 10 | $> ticat : : 11 | 12 | ## Example: 13 | $> ticat dummy : sleep 1s : echo hello 14 | 15 | ## Spaces(\s\t) are allowed but not necessary: 16 | $> ticat dummy:sleep 1s:echo hello 17 | ``` 18 | 19 | ## Display what will happen without execute a sequence 20 | ``` 21 | $> ticat : : : desc 22 | 23 | ## Exmaples: 24 | $> ticat dummy : desc 25 | $> ticat dummy : sleep 1s : echo hello : desc 26 | ``` 27 | 28 | ## Execute a sequence step by step 29 | The env key "sys.step-by-step" enable or disable the step-by-step feature: 30 | ``` 31 | $> ticat {sys.step-by-step = true} : : 32 | $> ticat {sys.step-by-step = on} : : 33 | $> ticat {sys.step = on} : : 34 | 35 | ## Enable it only for , to ask for confirmation from user 36 | $> ticat : {sys.step = on} : 37 | ``` 38 | 39 | A set of builtin commands could changes this env key for better usage: 40 | ``` 41 | ## Find these two commands: 42 | $> ticat cmds.tree dbg.step 43 | [step-by-step|step|s|S] 44 | - full-cmd: 45 | dbg.step-by-step 46 | - full-abbrs: 47 | dbg.step-by-step|step|s|S 48 | [on|yes|y|Y|1|+] 49 | 'enable step by step' 50 | - full-cmd: 51 | dbg.step-by-step.on 52 | - full-abbrs: 53 | dbg.step-by-step|step|s|S.on|yes|y|Y|1|+ 54 | - cmd-type: 55 | normal (quiet) 56 | - from: 57 | builtin 58 | [off|no|n|N|0|-] 59 | 'disable step by step' 60 | - full-cmd: 61 | dbg.step-by-step.off 62 | - full-abbrs: 63 | dbg.step-by-step|step|s|S.off|no|n|N|0|- 64 | - cmd-type: 65 | normal (quiet) 66 | - from: 67 | builtin 68 | 69 | ## Use these commands: 70 | $> ticat dbg.step.on : : : 71 | 72 | ## Enable step-by-step in the middle 73 | $> ticat : : dbg.step.on : 74 | 75 | ## Enable and save, after this all executions will need confirming 76 | $> ticat dbg.step.on : env.save 77 | ``` 78 | 79 | ## The "desc" command branch 80 | 81 | Overview 82 | ``` 83 | $> ticat cmds.tree.simple desc 84 | [desc] 85 | 'desc the flow about to execute' 86 | [simple] 87 | 'desc the flow about to execute in lite style' 88 | [skeleton] 89 | 'desc the flow about to execute, skeleton only' 90 | [dependencies] 91 | 'list the depended os-commands of the flow' 92 | [env-ops-check] 93 | 'desc the env-ops check result of the flow' 94 | [flow] 95 | 'desc the flow execution' 96 | [simple] 97 | 'desc the flow execution in lite style' 98 | ``` 99 | 100 | Exmaples of `desc`: 101 | ``` 102 | $> ticat : : : desc 103 | 104 | ## Examples: 105 | $> ticat dummy : desc 106 | $> ticat dummy : sleep 1s : echo hello : desc 107 | ``` 108 | 109 | ## Power/priority commands 110 | Some commands have "power" flag, these type of command can changes the sequence. 111 | Use "cmds.list " or "cmds.tree " can check a command's type. 112 | ``` 113 | ## Example: 114 | $> ticat cmds.tree dummy.power 115 | [power|p|P] 116 | 'power dummy cmd for testing' 117 | - full-cmd: 118 | dummy.power 119 | - full-abbrs: 120 | dummy|dmy|dm.power|p|P 121 | - cmd-type: 122 | power 123 | ``` 124 | 125 | The "desc" command have 3 flags: 126 | * quiet: it would display in the executing sequence(the boxes) 127 | * priority: it got to run first, then others could be executed. 128 | * power: it can change the sequence about to execute. 129 | ``` 130 | ## The command type of "desc" 131 | $> ticat cmds.tree desc 132 | [desc|d|D] 133 | 'desc the flow about to execute' 134 | - cmd-type: 135 | power (quiet) (priority) 136 | 137 | ## The usage of "desc" 138 | $> ticat : : : desc 139 | ## The actual execute order 140 | $> ticat desc : : : 141 | ## The actual execution: "desc" remove all the commands after display the sequence's info 142 | $> ticat desc [: : : ] 143 | ``` 144 | 145 | Other power commmands: 146 | ``` 147 | $> ticat cmd + 148 | [more|+] 149 | 'display rich info base on: 150 | * if in a sequence having 151 | * more than 1 other commands: show the sequence execution. 152 | * only 1 other command and 153 | * has no args and the other command is 154 | * a flow: show the flow execution. 155 | * not a flow: show the command or the branch info. 156 | * has args: find commands under the branch of the other command. 157 | * if not in a sequence and 158 | * has args: do global search. 159 | * has no args: show global help.' 160 | - cmd-type: 161 | power (quiet) (priority) 162 | - from: 163 | builtin 164 | ... 165 | ``` 166 | 167 | When have more than one priority commands in a sequence: 168 | ``` 169 | ## User input 170 | $> ticat : : : 171 | 172 | ## Actual execute order: 173 | $> ticat : : : 174 | ``` 175 | -------------------------------------------------------------------------------- /doc/spec/spec.md: -------------------------------------------------------------------------------- 1 | # Module developing zone 2 | 3 | * [Quick-start](../quick-start-mod.md) 4 | * [Examples: write modules in different languages](https://github.com/innerr/examples.ticat) 5 | * [How modules work together (with graphics)](../concept-graphics.md) 6 | 7 | ## Ticat specifications 8 | 9 | This is only **ticat**'s spec, a repo provides modules and flows will have it's own spec. 10 | 11 | * [Hub: list/add/disable/enable/purge](./hub.md) 12 | * [Command sequence](./seq.md) 13 | * [Command tree](./cmds.md) 14 | * [Env: list/get/set/save](./env.md) 15 | * [Abbrs of commands, env-keys and flows](./abbr.md) 16 | * [Flow: list/save/edit](./flow.md) 17 | * [Display control in executing](./display.md) 18 | * [Help info commands](./help.md) 19 | * [Local store dir](./local-store.md) 20 | * [Repo tree](./repo-tree.md) 21 | * [Module: env and args](./mod-interact.md) 22 | * [Module: meta file](./mod-meta.md) 23 | -------------------------------------------------------------------------------- /doc/usage/basic.md: -------------------------------------------------------------------------------- 1 | # Basic usage of ticat: build, run commands 2 | 3 | ### Build 4 | 5 | `golang` is needed to build ticat: 6 | ``` 7 | $> git clone https://github.com/innerr/ticat 8 | $> cd ticat 9 | $> make 10 | ``` 11 | Recommand to set `ticat/bin` to system `$PATH`, it's handy. 12 | 13 | ### Run a command 14 | 15 | Run a simple command, it does not thing but print a message: 16 | ``` 17 | $> ticat dummy 18 | dummy cmd here 19 | ``` 20 | Pass arg(s) to a command, `sleep` will pause for `3s` then wake up: 21 | ``` 22 | $> ticat sleep 20s 23 | .zzZZ .................... *\O/* 24 | ``` 25 | 26 | Use abbrs(or aliases) to call a command: 27 | ``` 28 | $> ticat slp 3s 29 | $> ticat slp dur=3s 30 | $> ticat slp d=3s 31 | ``` 32 | 33 | ### Run a command in the command-tree 34 | 35 | All commands are organized to a tree, 36 | the `sleep` and `dummy` commands are under the `root`, 37 | so we could call them directly. 38 | 39 | Another two commands does nothing: 40 | ``` 41 | $> ticat dummy.power 42 | power dummy cmd here 43 | $> ticat dummy.quiet 44 | quiet dummy cmd here 45 | ``` 46 | Do notice that `dummy` `dummy.power` `dummy.quiet` are totally different commands, 47 | they are in the same command-branch just because users can find related commands easily in this way. 48 | 49 | Display a command's info: 50 | ``` 51 | $> ticat dbg.echo :== 52 | [echo] 53 | 'print message from argv' 54 | - full-cmd: 55 | dbg.echo 56 | - args: 57 | message|msg|m|M = '' 58 | ``` 59 | From this we know that `dbg.echo` has an arg `message`, this arg has some abbrs: `msg` `m` `M`. 60 | 61 | Different ways to call the command: 62 | ``` 63 | $> ticat dbg.echo hello 64 | $> ticat dbg.echo "hello world" 65 | $> ticat dbg.echo msg=hello 66 | $> ticat dbg.echo m = hello 67 | $> ticat dbg.echo {M=hello} 68 | $> ticat dbg.echo {M = hello} 69 | ``` 70 | 71 | ### Use abbrs/aliases 72 | 73 | When we are searching commands, the output is like: 74 | ``` 75 | ... 76 | [hub|h|H] 77 | [clear|reset] 78 | 'remove all repos from hub' 79 | - full-cmd: 80 | hub.clear 81 | - full-abbrs: 82 | hub|h|H.clear|reset 83 | ... 84 | ``` 85 | From this we know a command `hub.clear`, 86 | the name `hub` has abbrs `h` `H`, 87 | and the `clear` has an alias `reset`. 88 | 89 | So `hub.clear` and `h.reset` are the same command: 90 | ``` 91 | $> ticat hub.clear 92 | $> ticat h.reset 93 | ``` 94 | -------------------------------------------------------------------------------- /doc/usage/env.md: -------------------------------------------------------------------------------- 1 | # Manipulate env key-values 2 | 3 | Env is a set of key-values, it's shared by all modules in an execution. 4 | 5 | For users, it's important to find out what keys a command(module or flow) need. 6 | 7 | ## Display a commands env-ops(read/write) 8 | 9 | For a module, `cmds` shows its detail included env-ops: 10 | ``` 11 | $> ticat cmds bench.run 12 | [run] 13 | 'pretend to run bench' 14 | - full-cmd: 15 | bench.run 16 | - env-ops: 17 | cluster.host = may-read 18 | cluster.port = read 19 | bench.scale = may-read 20 | bench.begin = write 21 | bench.end = write 22 | ... 23 | ``` 24 | 25 | For a flow, `desc` shows its unsatisfied env-ops: 26 | ``` 27 | $> ticat bench : desc 28 | --->>> 29 | [bench] 30 | ... 31 | <<<--- 32 | 33 | -------==------- 34 | 35 | 'cluster.port' 36 | - read by: 37 | [bench.load] 38 | [bench.run] 39 | - but not provided 40 | ``` 41 | An env key-value being read before write will cause a `FATAL` error, `risk` is normally fine. 42 | 43 | `+` can get instead `cmds` or `desc` for either command or flow: 44 | ``` 45 | $> ticat bench.run:+ 46 | $> ticat bench:+ 47 | ``` 48 | 49 | ## Set env key-values 50 | 51 | Modules are recommended to read env instead of reading from args as much as possible, 52 | it's the way to accomplish automatic assembly. 53 | 54 | So how to pass values to modules by env is important. 55 | The brackets `{``}` are used to set values during running. 56 | 57 | it's OK if the key does't exist in **env**: 58 | ``` 59 | $> tiat {display.width=40} 60 | ``` 61 | 62 | Use another command `env.ls` to show the modified value: 63 | ``` 64 | $> tiat {display.width=40} : env.ls display.width 65 | ``` 66 | 67 | Change multi key-values in one pair of brackets: 68 | ``` 69 | $> tiat {display.width=40 display.style=utf8} : env.ls display 70 | ``` 71 | 72 | ## Save env key-values 73 | 74 | By saving key-values to env, we don't need to type them down every time. 75 | 76 | Save changes of env by `env.save`, short name `e.+` 77 | ``` 78 | $> tiat {display.width=40} env.save 79 | $> tiat env.ls width 80 | display.width = 40 81 | $> tiat {display.width=60} env.save 82 | display.width = 60 83 | ``` 84 | 85 | ## Observe env key-values during running 86 | 87 | In the executing info box, the upper part has the current env key-values. 88 | ``` 89 | $> ticat {foo=bar} sleep 3m : dummy : dummy 90 | ┌───────────────────┐ 91 | │ stack-level: [1] │ 05-31 20:07:39 92 | ├───────────────────┴────────────────────────────┐ 93 | │ foo = bar │ 94 | ├────────────────────────────────────────────────┤ 95 | │ >> sleep │ 96 | │ duration = 3m │ 97 | │ dummy │ 98 | │ dummy │ 99 | └────────────────────────────────────────────────┘ 100 | ... 101 | ``` 102 | 103 | There are a large amount of key-values hidden, 104 | to show then we could use `verb` command. 105 | ``` 106 | $> ticat verb : dummy : dummy 107 | ``` 108 | 109 | `verb` shows maximum infos, shor name `v`. 110 | To show a little more info but not too much, we could use `v.+`: 111 | ``` 112 | $> ticat v.+ : dummy : dummy 113 | $> ticat v.+ 1 : dummy : dummy 114 | $> ticat v.+ 2 : dummy : dummy 115 | ``` 116 | 117 | `quiet` `q` chould totally hide the executing info: 118 | 119 | ``` 120 | $> ticat q : dummy : dummy 121 | ``` 122 | 123 | ## Display env key-values 124 | 125 | We know there are lots key-values besides we manually set into env, 126 | `env.flat` is the command to list them all, short name `e.f`: 127 | ``` 128 | $> ticat e.f 129 | ``` 130 | 131 | Find env key-values: 132 | ``` 133 | $> ticat e.f 134 | ``` 135 | 136 | Find env key-values in finding results 137 | ``` 138 | $> ticat e.f 139 | $> ticat e.f 140 | ``` 141 | 142 | The `v` `q` `v.+` commands alter some values to change the display behavior, 143 | we could find those key-values by: 144 | ``` 145 | $> ticat e.f display 146 | display.env = true 147 | display.env.default = false 148 | display.env.display = false 149 | display.env.layer = false 150 | display.env.sys = false 151 | display.env.sys.paths = false 152 | display.executor = true 153 | display.executor.end = false 154 | display.max-cmd-cnt = 7 155 | display.meow = false 156 | display.mod.quiet = false 157 | display.mod.realname = true 158 | display.one-cmd = false 159 | display.style = utf8 160 | display.utf8 = true 161 | display.width = 40 162 | ... 163 | ``` 164 | -------------------------------------------------------------------------------- /doc/usage/hub.md: -------------------------------------------------------------------------------- 1 | # Hub: get modules and flows from others 2 | 3 | ## What is hub? 4 | The **hub** is all local dirs linked with **ticat**, 5 | modules will be loaded on bootstrap by scanning these dirs: 6 | ``` 7 | ┌────────────────────────────────┐ 8 | │ Ticat Hub │ 9 | │ ┌──────────────────────────┐ │ 10 | │ │ Normal Local Dir │ │ 11 | │ │ ┌─────┐ ┌─────┐ │ │ 12 | │ │ │ Mod │ │ Mod │ ... │ │ 13 | │ │ └─────┘ └─────┘ │ │ 14 | │ └──────────────────────────┘ │ 15 | │ ┌──────────────────────────┐ │ 16 | │ │ Repo cloned to Local Dir │ │ 17 | │ │ ┌─────┐ │ │ 18 | │ │ │ Mod │ ... │ │ 19 | │ │ └─────┘ │ │ 20 | │ └──────────────────────────┘ │ 21 | │ ┌──────────────────────────┐ │ 22 | │ │ Repo cloned to Local Dir │ │ 23 | │ │ ┌─────┐ ┌─────┐ │ │ 24 | │ │ │ Mod │ │ Mod │ ... │ │ 25 | │ │ └─────┘ └─────┘ │ │ 26 | │ └──────────────────────────┘ │ 27 | └────────────────────────────────┘ 28 | ``` 29 | 30 | This command list all dirs in hub: 31 | ``` 32 | $> ticat hub 33 | ``` 34 | Or find dirs: 35 | ``` 36 | $> ticat hub 37 | ``` 38 | 39 | ## Add a git repo to hub 40 | These dirs could be a git repo, **ticat** will `git clone` it to local dir when: 41 | ``` 42 | $> ticat hub.add 43 | ``` 44 | or 45 | ``` 46 | $> ticat hub.add 47 | ``` 48 | 49 | If a repo has sub-repos([what is sub-repo](../spec/repo-tree.md)), 50 | they will be recursively clone to local too. 51 | 52 | All cloned repos store under a specific folder defined by env key `sys.paths.hub`. 53 | These dirs are under **ticat**'s management, are `managed` dirs. 54 | 55 | If we add an existed repo to hub, the repo will be updated by `git pull`. 56 | 57 | ## Init hub by adding default git address 58 | This command could add the default git address defined by env key `sys.hub.init-repo` to hub: 59 | ``` 60 | $> ticat hub.init 61 | ``` 62 | This repo has the most common modules, it's useful for new users. 63 | 64 | ## Update all managed repos 65 | ``` 66 | $> ticat hub.update 67 | ``` 68 | The disabled repos won't be updated. 69 | 70 | ## Add a local dir to hub 71 | Dirs in hub could be a normal dir added to **ticat** by: 72 | ``` 73 | $> ticat hub.add.local path= 74 | ``` 75 | 76 | The dir could be a repo manually cloned to local, 77 | **ticat** treat it as normal dir, they are `unmanaged`, 78 | it means **ticat** load modules from it but won't change anything in it. 79 | 80 | ## Disable/enable dirs 81 | We use find string as arg, to disable/enable multi dirs in one command: 82 | ``` 83 | $> ticat hub.disable 84 | $> ticat hub.enable 85 | ``` 86 | 87 | ## Permanently remove dirs from hub 88 | A dir must be disabled first, then use **purge** command to remove it: 89 | ``` 90 | $> ticat hub.purge 91 | ``` 92 | This command remove all disabled dirs: 93 | ``` 94 | $> ticat hub.purge.all 95 | ``` 96 | 97 | A managed dir will be totally deleted from file system. 98 | A normal(unmanaged) dir will be removed from hub but keep on file system. 99 | 100 | ## All commands about hub 101 | ``` 102 | $> ticat h:- 103 | [hub] 104 | 'list dir and repo info in hub' 105 | [clear] 106 | 'remove all repos from hub' 107 | [init] 108 | 'add and pull basic hub-repo to local' 109 | [add-and-update] 110 | 'add and pull a git address to hub' 111 | [local-dir] 112 | 'add a local dir (could be a git repo) to hub' 113 | [list] 114 | 'list dir and repo info in hub' 115 | [purge] 116 | 'remove an inactive repo from hub' 117 | [purge-all-inactive] 118 | 'remove all inactive repos from hub' 119 | [update-all] 120 | 'update all repos and mods defined in hub' 121 | [enable-repo] 122 | 'enable matched git repos in hub' 123 | [disable-repo] 124 | 'disable matched git repos in hub' 125 | [move-flows-to-dir] 126 | 'move all saved flows to a local dir (could be a git repo)' 127 | ``` 128 | -------------------------------------------------------------------------------- /doc/usage/user-manual.md: -------------------------------------------------------------------------------- 1 | # User manual 2 | 3 | * [Quick start](../../README.md) 4 | * [Basic: build, run commands](./basic.md) 5 | * [Hub: get modules and flows from others](./hub.md) 6 | * [Use commands](./cmds.md) 7 | * [Manipulate env key-values](./env.md) 8 | * [Use flows](./flow.md) 9 | -------------------------------------------------------------------------------- /doc/zen/why-abbrs.md: -------------------------------------------------------------------------------- 1 | # Zen: the choices we made 2 | 3 | ## Why so many abbrs and aliases? 4 | 5 | Two reasons: 6 | * Reduce memorizing works 7 | * To have a long command name 8 | 9 | ## Reduce memorizing works 10 | 11 | With the ability of flexable ad-hot feature assembling, 12 | **ticat** users also have overwhelming infos. 13 | 14 | We did all things we can to reduce this pressure: 15 | * Full search supporting for all things, recommend users don't look after things, just search. 16 | * Various way to display infos, some are focus and detailed, some are large range and essential. 17 | * ... 18 | 19 | Abbrs supporting is one of them, 20 | module developers are suggested to setup possible aliases, or even misspellings. 21 | In that, users don't need to pay attention to memorize commands and arg-names, 22 | they could just have a roughly guess. 23 | 24 | For example, for command `tpcc.run`, it has a arg named `terminal` by a popular implementation. 25 | If this arg have aliases `term` `thread` `threads`, that users can hardly make mistakes. 26 | 27 | ## To have a long command name 28 | 29 | Some commands have relatively complicated meanings, for that, a long command name is a good choice. 30 | 31 | But long name is unfriendly in use, it need more momorizing and more typing. 32 | 33 | With abbrs supporting, these commands could have a long and meaningful name, yet still easy to use. 34 | 35 | Realname will display no matter where an abbr is displaying, to show the command meaning. 36 | -------------------------------------------------------------------------------- /doc/zen/why-cli.md: -------------------------------------------------------------------------------- 1 | # Zen: the choices we made 2 | 3 | ## Why cli? 4 | 5 | Q: A normal component platform will use rpc as component API, 6 | then use config files to assemble pieces, or maybe use web-page-based UI. 7 | why cli based component platform? 8 | 9 | ## Cost is all 10 | 11 | Cost is all in engineering. (it toke me more than 10yrs to realize that, 12 | at my early days I was sticking on 'quality'). 13 | 14 | Since **ticat** is for engineers, 15 | using cli as platform do not compromise much usabilities, 16 | but can greatly reduce the cost of writing a component. 17 | 18 | By lower the bar(cost) of component writing, 19 | small solutions could move (from manually executings and casual scripts) into the platform, 20 | that makes a health and strong ecosystem. 21 | -------------------------------------------------------------------------------- /doc/zen/why-hub.md: -------------------------------------------------------------------------------- 1 | # Zen: the choices we made 2 | 3 | ## Why use git repo to publish components? 4 | 5 | Q: normally a component platform will build it's own center service, 6 | why **ticat** just use git repos? 7 | 8 | The reason is to build a better community. 9 | 10 | ### User-centered VS official-centered 11 | 12 | Most platforms are official-centered: 13 | * The authority publish componets. 14 | * Users pull their needed componets. 15 | In this model it's hard to publish user-owned componets. 16 | 17 | Some platform provide mirror tools and allow users to establish their center service. 18 | But notice, the tools and maybe some access setting still under authority's control. 19 | 20 | With git repo as a publish service, 21 | **ticat** try to build a user-centered model, 22 | Users can decide what to add to local disk, 23 | And an important thing: anyone could become a publisher without any cost: 24 | * Fork and edit the repo. 25 | * Use git/github's access setting. 26 | 27 | With this loose-auth model, we hope to constructe an active and layered ecosystem. 28 | 29 | The layered community means, a developer don't need to be pro to contribute (to a specific group), 30 | he could write crappy code at the very beginning, 31 | but still can be a publisher sharing his work and assemble with all the pro modules, 32 | once he have some good pieces, adapting his work into a higher-level publisher is very easy. 33 | 34 | With **ticat**, the group can provide a easy-to-use environment for developer to get to know the project 35 | (by many runnable flows), and a smooth path from beginner to core coding. 36 | 37 | By that, we recommend "better than now" rule, 38 | his work no need to be good, just need to be better (than now). 39 | 40 | In this way, the project could partly improve, 41 | and the community can gradually evolve. 42 | -------------------------------------------------------------------------------- /doc/zen/why-not-async.md: -------------------------------------------------------------------------------- 1 | # Zen: the choices we made 2 | 3 | ## Why not support async/concurrent executing? 4 | 5 | We built a immature version of **ticat** before call `ti.sh`, 6 | it do support async/concurrent executing with a keyword `go`. 7 | 8 | But in practices we found out that these were rarely used, 9 | it has limited demands. 10 | 11 | So we decide not to support async/concurrent executing in **ticat** for now. 12 | Once we need them we will provide them. 13 | 14 | Components can do async executing by themselves now, 15 | an example: 16 | ``` 17 | $> ticat bench.async-run : ... : ... : bench.wait-async 18 | ``` 19 | -------------------------------------------------------------------------------- /doc/zen/why-not-pipe.md: -------------------------------------------------------------------------------- 1 | # Zen: the choices we made 2 | 3 | ## Why not just use unix-pipe? 4 | 5 | ### Executing order 6 | 7 | Even **ticat** use unix-pipe style, but it's not the same. 8 | 9 | The executing orders are different: 10 | * In pipe, all commands launch at the same time 11 | * In **ticat**, commands launch one by one 12 | 13 | Apparently, **ticat** do this to meet the "workflow control" demand, so pipe is not fit here. 14 | 15 | ### Unix-pipe is weak, ticat env is strong 16 | 17 | * Unix-pipe is anonymous, hard to define and force to apply a specific protocol in concept. 18 | - named-pipe(mkfifo) could solve this, but recycling will be a hard job. 19 | * There is only one pipe between commands, which we can only passing one type of data. 20 | 21 | Of cause we could define protocols on pipe to solve all those, 22 | but it will make it inconvenient to write a component(eg, in bash). 23 | 24 | In **ticat**, env key-values can be considered as another form of named-pipe, 25 | with a name, a key-value could bind to a format definition. 26 | 27 | There is no recycling issue about env key-values, 28 | even it have, **ticat** implemented session model, could handle it easily. 29 | 30 | The env-ops(read/write) in a **ticat** command sequence will be checked before executing, 31 | commands are required to register what keys them will read or write, 32 | read before write will cause fatal report. 33 | So the **ticat** model is a managed named-pipe-like system. 34 | 35 | A **ticat** flow can be called in another flow(command sequence), 36 | which forms callstacks, this is important in complicated assemblings. 37 | To support this, unix-pipe is far not enough. 38 | 39 | The callstack depth display in executing (stack-level): 40 | ``` 41 | ┌───────────────────┐ 42 | │ stack-level: [3] │ 06-02 04:51:45 43 | ├───────────────────┴────────────────────────────┐ 44 | │ bench.scale = 4 │ 45 | │ cluster.port = 4000 │ 46 | ├────────────────────────────────────────────────┤ 47 | │ local.build │ 48 | │ >> cluster.local │ 49 | │ port = '' │ 50 | │ host = 127.0.0.1 │ 51 | │ cluster.restart │ 52 | │ ben(=bench).load │ 53 | │ ben(=bench).run │ 54 | └────────────────────────────────────────────────┘ 55 | ``` 56 | -------------------------------------------------------------------------------- /doc/zen/why-tags.md: -------------------------------------------------------------------------------- 1 | # Zen: the choices we made 2 | 3 | ## Why use tags, and it looks like very informal, just some words in help string. 4 | 5 | Using tags to declair "what we are" is a regular thing, 6 | we use some conventional tag to connect module authors and users, 7 | to let them know "how to use" and "how to tell people how to use". 8 | 9 | As a platform (even is a small one) searching is the most important thing, 10 | **ticat** can do a excellent job in searching, 11 | any properties in a command could match by keywords: 12 | command name, help string, arg names, env ops, anyting. 13 | 14 | Since we have full text indexing, a common word in help string should be enough as a tag. 15 | We recommend adding prefix `@` could improve the searching accuracy. 16 | So tags will looks like `@ready` `@selftest` embedded in help string. 17 | -------------------------------------------------------------------------------- /doc/zen/why-ticat.md: -------------------------------------------------------------------------------- 1 | # Zen: the choices we made 2 | 3 | ## Why ticat, why we need a new platform, is that anything the existing ones can't do? 4 | 5 | It's overdetermined to list things "**ticat** can, others can't". 6 | 7 | It's all about cost, 8 | **ticat** meet our demand with significantly low cost. 9 | (both for users and component developers) 10 | 11 | However, here are some features we rarely saw if not in **ticat**: 12 | * Self-sufficient component model, make pieces can easily work together. 13 | * Low cost ad-hot componet assembling, have maximum flexibility to cover any requement. 14 | * Low cost componet developing support. 15 | * User-centered publish model and health community. 16 | -------------------------------------------------------------------------------- /doc/zen/why-tree.md: -------------------------------------------------------------------------------- 1 | # Zen: the choices we made 2 | 3 | ## Why commands and env key-values are in tree form 4 | 5 | In fact, most command line tools choose 2~3 depth sub-command organizating, 6 | for example: 7 | ``` 8 | $> tiup cluster start 9 | $> tiup cluster deploy 10 | ``` 11 | 12 | The differences are little: 13 | * Ticat join the command path with `.` into an id like `cluster.start` 14 | * Ticat has unlimit depth 15 | 16 | The reason is to deal with huge number of commands, 17 | **ticat** is a platform with huge amount of commands from different authors, 18 | the tree form provides a namespace mechanism to avoid command name conflictings. 19 | 20 | The branch path could be used for a specific concept, 21 | the authors provide tools in this sub-tree, 22 | and the uses can explore this branch for specific tools. 23 | 24 | On the end-user side, 25 | looking through the command tree to find what we want is not sugguested, 26 | searching by keywords is a better way. 27 | 28 | The reasons are the same in env key-values. 29 | -------------------------------------------------------------------------------- /doc/zen/zen.md: -------------------------------------------------------------------------------- 1 | # Zen: the choices we made 2 | 3 | * [Why ticat](./why-ticat.md) 4 | * [Why use cli as component platform](./why-cli.md) 5 | * [Why not use unix pipe](./why-not-pipe.md) 6 | * [Why the usage so weird, especially the `+` and `-`](./why-weird.md) 7 | * [Why use tags](./why-tags.md) 8 | * [Why so many abbrs and aliases](./why-abbrs.md) 9 | * [Why commands and env key-values are in tree form](./why-tree.md) 10 | * [Why use git repo to distribute componets](./why-hub.md) 11 | * [Why not support async/concurrent executing](./why-not-async.md) 12 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/innerr/ticat 2 | 3 | go 1.16 4 | 5 | require ( 6 | github.com/mattn/go-shellwords v1.0.11 7 | github.com/peterh/liner v1.2.1 8 | ) 9 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/mattn/go-runewidth v0.0.3 h1:a+kO+98RDGEfo6asOGMmpodZq4FNtnGP54yps8BzLR4= 2 | github.com/mattn/go-runewidth v0.0.3/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= 3 | github.com/mattn/go-shellwords v1.0.11 h1:vCoR9VPpsk/TZFW2JwK5I9S0xdrtUq2bph6/YjEPnaw= 4 | github.com/mattn/go-shellwords v1.0.11/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y= 5 | github.com/peterh/liner v1.2.1 h1:O4BlKaq/LWu6VRWmol4ByWfzx6MfXc5Op5HETyIy5yg= 6 | github.com/peterh/liner v1.2.1/go.mod h1:CRroGNssyjTd/qIG2FyxByd2S8JEAZXBl4qUrZf8GS0= 7 | -------------------------------------------------------------------------------- /install.sh: -------------------------------------------------------------------------------- 1 | function printf() 2 | { 3 | local os=`uname` 4 | if [ "${os}" = 'Darwin' ]; then 5 | echo "${@}" 6 | else 7 | echo -e "${@}" 8 | fi 9 | } 10 | 11 | function check_cmd_exists() 12 | { 13 | local cmd="${1}" 14 | if ! [ -x "$(command -v ${cmd})" ]; then 15 | echo "[:(] '"${cmd}"' not found, please install it first" >&2 16 | return 1 17 | fi 18 | } 19 | 20 | function download_bin() 21 | { 22 | local repo="${1}" 23 | local bin_name="${2}" 24 | local dir="${3}" 25 | local token="Authorization: token ${4}" 26 | 27 | local os_type=`uname | awk '{print tolower($0)}'` 28 | echo "[:-] detected os type: ${os_type}" >&2 29 | 30 | local long_name="pre-builtin bin '${bin_name}' on '${repo}' for '${os_type}'" 31 | 32 | check_cmd_exists 'curl' 33 | 34 | local json=`curl --proto '=https' --tlsv1.2 -sSf -H "${token}" "https://api.${repo}/releases/latest"` 35 | 36 | local ver=`echo "${json}" | grep '"tag_name": ' | awk -F '"' '{print $(NF-1)}'` 37 | if [ -z "${ver}" ]; then 38 | echo "***" >&2 39 | echo "${json}" >&2 40 | echo "***" >&2 41 | echo "[:(] ${long_name} version not found or can't be downloaded, exiting" >&2 42 | return 1 43 | fi 44 | 45 | local info=`echo "${json}" | \ 46 | grep '"assets": ' -A 99999 | \ 47 | grep '"name": \|"browser_download_url": ' | \ 48 | { grep "${bin_name}_${os_type}" || test $? = 1; } | \ 49 | awk -F '": "' '{print $2}' | \ 50 | awk -F '"' '{print $1}'` 51 | 52 | local res_name=`echo "${info}" | { grep -v 'https://' || test $? = 0; }` 53 | if [ -z "${res_name}" ]; then 54 | echo "[:(] ${long_name} not found, exiting" >&2 55 | return 1 56 | fi 57 | 58 | local cnt=`echo "${res_name}" | wc -l | awk '{print $1}'` 59 | if [ "${cnt}" != '1' ]; then 60 | echo "***" >&2 61 | echo "${res_name}" | awk '{print " - "$0}' >&2 62 | echo "***" >&2 63 | echo "[:(] error: more than one (${cnt}) resource of ${long_name}, exiting" >&2 64 | return 1 65 | fi 66 | 67 | local download_url=`echo "${info}" | tail -n 1` 68 | 69 | local hash_val=`echo "${res_name}" | awk -F '_' '{print $NF}' | awk -F '.' '{print $1}'` 70 | local hash_bin=`echo "${res_name}" | awk -F '_' '{print $(NF-1)}'` 71 | 72 | local bin_path="${dir}/${bin_name}" 73 | 74 | echo "[:)] located version ${ver}: ${long_name}" 75 | echo " - ${hash_bin}: ${hash_val}" 76 | echo " - url: ${download_url}" 77 | echo " - download to: ${bin_path}" 78 | 79 | curl --proto '=https' --tlsv1.2 -sSf -kL -H "${token}" "${download_url}" > "${bin_path}" 80 | chmod +x "${bin_path}" 81 | 82 | echo "[:)] downloaded" 83 | } 84 | 85 | function download_and_install_ticat() 86 | { 87 | local download_rate_limit_token="ghp_${1}" 88 | if [ -z "${2+x}" ]; then 89 | local mods_repo='' 90 | else 91 | local mods_repo="${2}" 92 | fi 93 | 94 | check_cmd_exists 'git' 95 | check_cmd_exists 'awk' 96 | 97 | local title='\033[1;94m' 98 | local green='\033[0;32m' 99 | local gray='\033[38;5;8m' 100 | local gray='\033[0;35m' 101 | local orange='\033[0;33m' 102 | local nc='\033[0m' 103 | 104 | local ticat_repo='github.com/repos/innerr/ticat' 105 | printf "${title}==> download ticat${nc}" 106 | download_bin "${ticat_repo}" 'ticat' '.' "${download_rate_limit_token}" 2>&1 | awk '{print " * "$0}' 107 | 108 | echo 109 | if [ ! -z "${mods_repo}" ]; then 110 | printf "${title}==> fetch components from '${mods_repo}'${nc}" 111 | ./ticat display.color.on : display.utf.off : display.width 90 : hub.add "${mods_repo}" 2>&1 | awk '{print " * "$0}' 112 | else 113 | printf "${title}==> init ticat with basic component repos${nc}" 114 | ./ticat display.color.on : display.utf.off : display.width 90 : noop 2>&1 | awk '{print " * "$0}' 115 | fi 116 | 117 | echo 118 | printf "${title}==> add ticat to \$PATH${nc}" 119 | ./ticat display.color.on : install.ticat 2>&1 | awk '{print " * "$0}' 120 | 121 | echo 122 | printf "${green}==> Command ${orange}./ticat${green} is ready now, ${orange}ticat${green} is available after relogin${nc}" 123 | printf " ${gray}ticat: workflow automating in unix-pipe style${nc}" 124 | printf " ${gray}try: $>./ticat${nc}" 125 | } 126 | 127 | set -eo pipefail 128 | mods_repo="${1}" 129 | set -u 130 | 131 | download_and_install_ticat 'NYrOv0JuQ8iZ6cEnOTzdaTfh7ovx2Q2iwEQX' "${mods_repo}" 132 | -------------------------------------------------------------------------------- /pkg/cli/display/abbrs.go: -------------------------------------------------------------------------------- 1 | package display 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/innerr/ticat/pkg/core/model" 7 | ) 8 | 9 | // TODO: dump more info, eg: full path 10 | func DumpEnvAbbrs(cc *model.Cli, env *model.Env, indentSize int) { 11 | dumpEnvAbbrs(cc.Screen, cc.EnvAbbrs, env, cc.Cmds.Strs.AbbrsSep, indentSize, 0) 12 | } 13 | 14 | func dumpEnvAbbrs( 15 | screen model.Screen, 16 | abbrs *model.EnvAbbrs, 17 | env *model.Env, 18 | abbrsSep string, 19 | indentSize int, 20 | indent int) { 21 | 22 | if abbrs == nil { 23 | return 24 | } 25 | prt := func(msg string) { 26 | if indent >= 0 { 27 | screen.Print(rpt(" ", indentSize*indent) + msg + "\n") 28 | } 29 | } 30 | 31 | name := strings.Join(abbrs.Abbrs(), abbrsSep) 32 | prt(ColorKey("["+name+"]", env)) 33 | 34 | for _, name := range abbrs.SubNames() { 35 | dumpEnvAbbrs(screen, abbrs.GetSub(name), env, abbrsSep, indentSize, indent+1) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /pkg/cli/display/args.go: -------------------------------------------------------------------------------- 1 | package display 2 | 3 | import ( 4 | "github.com/innerr/ticat/pkg/core/model" 5 | ) 6 | 7 | func DumpProvidedArgs(env *model.Env, args *model.Args, argv model.ArgVals, colorize bool) (output []string) { 8 | for _, k := range args.Names() { 9 | v, provided := argv[k] 10 | if !provided || !v.Provided { 11 | continue 12 | } 13 | val := mayQuoteStr(mayMaskSensitiveVal(env, k, v.Raw)) 14 | if colorize { 15 | line := ColorArg(k, env) + ColorSymbol(" = ", env) + val 16 | output = append(output, line) 17 | } else { 18 | line := k + " = " + val 19 | output = append(output, line) 20 | } 21 | } 22 | return 23 | } 24 | 25 | func DumpSysArgs(env *model.Env, sysArgv model.SysArgVals, colorize bool) (output []string) { 26 | for k, v := range sysArgv { 27 | v = mayMaskSensitiveVal(env, k, v) 28 | if colorize { 29 | line := ColorExplain("[sys] ", env) + ColorArg(k, env) + 30 | ColorSymbol(" = ", env) + mayQuoteStr(v) 31 | output = append(output, line) 32 | } else { 33 | line := "[sys] " + k + " = " + mayQuoteStr(v) 34 | output = append(output, line) 35 | } 36 | } 37 | return 38 | } 39 | 40 | func DumpEffectedArgs( 41 | env *model.Env, 42 | arg2env *model.Arg2Env, 43 | args *model.Args, 44 | argv model.ArgVals, 45 | writtenKeys FlowWrittenKeys, 46 | stackDepth int) (output []string) { 47 | 48 | for _, k := range args.Names() { 49 | defV := args.DefVal(k, stackDepth) 50 | line := ColorArg(k, env) + " " + ColorSymbol("=", env) + " " 51 | v, provided := argv[k] 52 | if provided && v.Provided { 53 | line += mayQuoteStr(v.Raw) 54 | } else { 55 | if len(defV) == 0 { 56 | continue 57 | } 58 | key, hasMapping := arg2env.GetEnvKey(k) 59 | _, inEnv := env.GetEx(key) 60 | if hasMapping && inEnv { 61 | continue 62 | } 63 | if hasMapping && writtenKeys[key] { 64 | continue 65 | } 66 | line += mayQuoteStr(defV) 67 | } 68 | output = append(output, line) 69 | } 70 | return 71 | } 72 | -------------------------------------------------------------------------------- /pkg/cli/display/dep.go: -------------------------------------------------------------------------------- 1 | package display 2 | 3 | import ( 4 | "fmt" 5 | "os/exec" 6 | "sort" 7 | 8 | "github.com/innerr/ticat/pkg/core/model" 9 | ) 10 | 11 | func GatherOsCmdsExistingInfo(deps model.Depends) (foundOsCmds map[string]bool, osCmds []string, missedOsCmds int) { 12 | foundOsCmds = map[string]bool{} 13 | for osCmd, _ := range deps { 14 | exists := isOsCmdExists(osCmd) 15 | foundOsCmds[osCmd] = exists 16 | if !exists { 17 | missedOsCmds += 1 18 | } 19 | osCmds = append(osCmds, osCmd) 20 | } 21 | sort.Strings(osCmds) 22 | return 23 | } 24 | 25 | func DumpDepends( 26 | screen model.Screen, 27 | env *model.Env, 28 | deps model.Depends) (hasMissedOsCmd bool) { 29 | 30 | if len(deps) == 0 { 31 | return 32 | } 33 | 34 | foundOsCmds, osCmds, missedOsCmds := GatherOsCmdsExistingInfo(deps) 35 | 36 | sep := env.Get("strs.cmd-path-sep").Raw 37 | 38 | if missedOsCmds > 0 { 39 | PrintErrTitle(screen, env, 40 | "missed depended os-commands.", 41 | "", 42 | "the needed os-commands below are not installed:") 43 | } else { 44 | PrintTipTitle(screen, env, 45 | "depended os-commands are all installed.", 46 | "", 47 | "this flow need these os-commands below to execute:") 48 | } 49 | 50 | for _, osCmd := range osCmds { 51 | if missedOsCmds > 0 && foundOsCmds[osCmd] { 52 | continue 53 | } 54 | cmds := deps[osCmd] 55 | screen.Print(ColorCmd(fmt.Sprintf("[%s]\n", osCmd), env)) 56 | 57 | // TODO: sort cmds 58 | for _, info := range cmds { 59 | screen.Print(" " + ColorHelp(fmt.Sprintf("'%s'\n", info.Reason), env)) 60 | screen.Print(" " + ColorCmd(fmt.Sprintf("[%s]\n", info.Cmd.DisplayPath(sep, true)), env)) 61 | } 62 | } 63 | 64 | return missedOsCmds > 0 65 | } 66 | 67 | func isOsCmdExists(cmd string) bool { 68 | path, err := exec.LookPath(cmd) 69 | return err == nil && len(path) > 0 70 | } 71 | -------------------------------------------------------------------------------- /pkg/cli/display/env.go: -------------------------------------------------------------------------------- 1 | package display 2 | 3 | import ( 4 | "sort" 5 | "strings" 6 | 7 | "github.com/innerr/ticat/pkg/core/model" 8 | ) 9 | 10 | func DumpEnvTree(screen model.Screen, env *model.Env, indentSize int) { 11 | lines, _ := dumpEnv(env, nil, true, true, true, true, nil, indentSize) 12 | for _, line := range lines { 13 | screen.Print(line + "\n") 14 | } 15 | } 16 | 17 | func DumpEssentialEnvFlattenVals(screen model.Screen, env *model.Env, findStrs ...string) { 18 | filterPrefixs := []string{ 19 | "session", 20 | "strs.", 21 | "sys.", 22 | "display.", 23 | } 24 | flatten := env.Flatten(false, filterPrefixs, true) 25 | dumpEnvFlattenVals(screen, env, flatten, findStrs...) 26 | } 27 | 28 | func DumpEnvFlattenVals(screen model.Screen, env *model.Env, findStrs ...string) { 29 | flatten := env.Flatten(true, nil, true) 30 | dumpEnvFlattenVals(screen, env, flatten, findStrs...) 31 | } 32 | 33 | func KeyValueDisplayStr(key string, value string, env *model.Env) string { 34 | value = mayMaskSensitiveVal(env, key, value) 35 | return ColorKey(key, env) + ColorSymbol(" = ", env) + mayQuoteStr(value) 36 | } 37 | 38 | func KeyValueDisplayStrEx(key string, value string, env *model.Env, envKeysInfo *model.EnvKeysInfo) (string, int) { 39 | extraLen := ColorExtraLen(env, "symbol", "key") 40 | if envKeysInfo != nil { 41 | keyInfo := envKeysInfo.Get(key) 42 | if keyInfo != nil { 43 | if len(keyInfo.InvisibleDisplay) != 0 { 44 | value = keyInfo.InvisibleDisplay 45 | } else if keyInfo.DisplayLen != 0 { 46 | extraLen += len(value) - keyInfo.DisplayLen 47 | } 48 | } 49 | } 50 | value = mayMaskSensitiveVal(env, key, value) 51 | return ColorKey(key, env) + ColorSymbol(" = ", env) + mayQuoteStr(value), extraLen 52 | } 53 | 54 | func dumpEnvFlattenVals(screen model.Screen, env *model.Env, flatten map[string]string, findStrs ...string) { 55 | var keys []string 56 | for k, _ := range flatten { 57 | keys = append(keys, k) 58 | } 59 | sort.Strings(keys) 60 | for _, k := range keys { 61 | v := flatten[k] 62 | if len(findStrs) != 0 { 63 | notMatched := false 64 | for _, findStr := range findStrs { 65 | if strings.Index(k, findStr) < 0 && 66 | strings.Index(v, findStr) < 0 { 67 | notMatched = true 68 | break 69 | } 70 | } 71 | if notMatched { 72 | continue 73 | } 74 | } 75 | screen.Print(KeyValueDisplayStr(k, v, env) + "\n") 76 | } 77 | } 78 | 79 | func dumpEnv( 80 | env *model.Env, 81 | envKeysInfo *model.EnvKeysInfo, 82 | printEnvLayer bool, 83 | printDefEnv bool, 84 | printRuntimeEnv bool, 85 | printEnvStrs bool, 86 | filterPrefixs []string, 87 | indentSize int) (res []string, extraLens []int) { 88 | 89 | sep := env.Get("strs.env-path-sep").Raw 90 | if !printRuntimeEnv { 91 | sysPrefix := env.Get("strs.env-sys-path").Raw + sep 92 | filterPrefixs = append(filterPrefixs, sysPrefix) 93 | } 94 | if !printEnvStrs { 95 | strsPrefix := env.Get("strs.env-strs-path").Raw + sep 96 | filterPrefixs = append(filterPrefixs, strsPrefix) 97 | } 98 | 99 | if !printEnvLayer { 100 | flatten := env.Flatten(printDefEnv, filterPrefixs, true) 101 | var keys []string 102 | for k, _ := range flatten { 103 | keys = append(keys, k) 104 | } 105 | sort.Strings(keys) 106 | for _, k := range keys { 107 | v := mayMaskSensitiveVal(env, k, flatten[k]) 108 | res = append(res, ColorKey(k, env)+ColorSymbol(" = ", env)+v) 109 | extraLens = append(extraLens, ColorExtraLen(env, "key", "symbol")) 110 | } 111 | } else { 112 | dumpEnvLayer(env, env, envKeysInfo, printEnvLayer, printDefEnv, filterPrefixs, &res, &extraLens, indentSize, 0) 113 | } 114 | return 115 | } 116 | 117 | func dumpEnvLayer( 118 | env *model.Env, 119 | topEnv *model.Env, 120 | envKeysInfo *model.EnvKeysInfo, 121 | printEnvLayer bool, 122 | printDefEnv bool, 123 | filterPrefixs []string, 124 | res *[]string, 125 | extraLens *[]int, 126 | indentSize int, 127 | depth int) { 128 | 129 | if env.LayerType() == model.EnvLayerDefault && !printDefEnv { 130 | return 131 | } 132 | var output []string 133 | var outputExtraLens []int 134 | indent := rpt(" ", depth*indentSize) 135 | keys, _ := env.Pairs() 136 | sort.Strings(keys) 137 | for _, k := range keys { 138 | v := env.Get(k) 139 | filtered := false 140 | // Not filter default layer values 141 | for _, filterPrefix := range filterPrefixs { 142 | if len(filterPrefix) != 0 && strings.HasPrefix(k, filterPrefix) && env.LayerType() != model.EnvLayerDefault { 143 | filtered = true 144 | break 145 | } 146 | } 147 | if !filtered { 148 | kvStr, extraLen := KeyValueDisplayStrEx(k, v.Raw, topEnv, envKeysInfo) 149 | output = append(output, indent+"- "+kvStr) 150 | outputExtraLens = append(outputExtraLens, extraLen) 151 | } 152 | } 153 | if env.Parent() != nil { 154 | dumpEnvLayer(env.Parent(), topEnv, envKeysInfo, printEnvLayer, printDefEnv, 155 | filterPrefixs, &output, &outputExtraLens, indentSize, depth+1) 156 | } 157 | if len(output) != 0 { 158 | *res = append(*res, indent+ColorSymbol("["+env.LayerTypeName()+"]", topEnv)) 159 | *extraLens = append(*extraLens, ColorExtraLen(topEnv, "symbol")) 160 | *res = append(*res, output...) 161 | *extraLens = append(*extraLens, outputExtraLens...) 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /pkg/cli/display/frame.go: -------------------------------------------------------------------------------- 1 | package display 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | type FrameChars struct { 8 | V string 9 | H string 10 | 11 | // Sudoku positions 12 | P1 string 13 | P2 string 14 | P3 string 15 | P4 string 16 | P5 string 17 | P6 string 18 | P7 string 19 | P8 string 20 | P9 string 21 | } 22 | 23 | func FrameCharsHeavy() *FrameChars { 24 | return &FrameChars{ 25 | "|", "=", 26 | "=", "=", "=", 27 | "=", "=", "=", 28 | "=", "=", "=", 29 | } 30 | } 31 | 32 | func FrameCharsUtf8Heavy() *FrameChars { 33 | return &FrameChars{ 34 | "┃", "━", 35 | "┏", "┳", "┓", 36 | "┣", "╋", "┫", 37 | "┗", "┻", "┛", 38 | } 39 | } 40 | 41 | func FrameCharsUtf8() *FrameChars { 42 | return &FrameChars{ 43 | "│", "─", 44 | "┌", "┬", "┐", 45 | "├", "┼", "┤", 46 | "└", "┴", "┘", 47 | } 48 | } 49 | 50 | func FrameCharsUtf8Colored(colorCode int) *FrameChars { 51 | prefix := "\033[38;5;" + fmt.Sprintf("%d", colorCode) + "m" 52 | render := func(s string) string { 53 | return prefix + s + "\033[0m" 54 | } 55 | return &FrameChars{ 56 | render("│"), render("─"), 57 | render("┌"), render("┬"), render("┐"), 58 | render("├"), render("┼"), render("┤"), 59 | render("└"), render("┴"), render("┘"), 60 | } 61 | } 62 | 63 | func FrameCharsAsciiColored(colorCode int) *FrameChars { 64 | prefix := "\033[38;5;" + fmt.Sprintf("%d", colorCode) + "m" 65 | render := func(s string) string { 66 | return prefix + s + "\033[0m" 67 | } 68 | return &FrameChars{ 69 | render("|"), render("-"), 70 | render("+"), render("+"), render("+"), 71 | render("+"), render("+"), render("+"), 72 | render("+"), render("+"), render("+"), 73 | } 74 | } 75 | 76 | func FrameCharsAscii() *FrameChars { 77 | return &FrameChars{ 78 | "|", "-", 79 | "+", "+", "+", 80 | "+", "+", "+", 81 | "+", "+", "+", 82 | } 83 | } 84 | 85 | func FrameCharsNoSlash() *FrameChars { 86 | return &FrameChars{ 87 | "-", "-", 88 | "+", "+", "+", 89 | "+", "+", "+", 90 | "+", "+", "+", 91 | } 92 | } 93 | 94 | func FrameCharsNoBorder() *FrameChars { 95 | return &FrameChars{ 96 | "|", " ", 97 | ".", " ", ".", 98 | " ", "+", " ", 99 | "+", " ", "+", 100 | } 101 | } 102 | 103 | func FrameCharsNoCorner() *FrameChars { 104 | return &FrameChars{ 105 | "|", "-", 106 | " ", " ", " ", 107 | " ", " ", " ", 108 | " ", " ", " ", 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /pkg/cli/display/help.go: -------------------------------------------------------------------------------- 1 | package display 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/innerr/ticat/pkg/core/model" 7 | "github.com/innerr/ticat/pkg/utils" 8 | ) 9 | 10 | func PrintGlobalHelp(cc *model.Cli, env *model.Env) { 11 | // TODO: use man page instead of help 12 | PrintSelfHelp(cc.Screen, env) 13 | return 14 | 15 | if len(cc.Helps.Sections) == 0 { 16 | PrintSelfHelp(cc.Screen, env) 17 | return 18 | } 19 | 20 | // TODO: with color output this is not right, disable it by setting to a very big value 21 | _, width := utils.GetTerminalWidth(50, 100) 22 | width = 4096 23 | 24 | pln := func(line string) { 25 | line = DecodeColor(line, env) 26 | if len(line) <= width { 27 | cc.Screen.Print(line + "\n") 28 | return 29 | } 30 | 31 | indent := 0 32 | for _, char := range line { 33 | if char != ' ' && char != '\t' { 34 | break 35 | } 36 | indent += 1 37 | } 38 | prefix := line[:indent] 39 | 40 | printWithPrefix := func(printed bool, line string) { 41 | if printed { 42 | cc.Screen.Print(prefix) 43 | cc.Screen.Print(strings.TrimLeft(line, " \t") + "\n") 44 | } else { 45 | cc.Screen.Print(line + "\n") 46 | } 47 | } 48 | 49 | printed := false 50 | for { 51 | if len(line) > width { 52 | printWithPrefix(printed, line[:width]) 53 | line = line[width:] 54 | printed = true 55 | continue 56 | } else { 57 | printWithPrefix(printed, line) 58 | } 59 | break 60 | } 61 | } 62 | 63 | for _, help := range cc.Helps.Sections { 64 | PrintTipTitle(cc.Screen, env, help.Title) 65 | for _, line := range help.Text { 66 | pln(line) 67 | } 68 | } 69 | } 70 | 71 | func PrintSelfHelp(screen model.Screen, env *model.Env) { 72 | pln := func(texts ...string) { 73 | for _, text := range texts { 74 | if len(text) == 0 { 75 | screen.Print("\n") 76 | } else { 77 | screen.Print(" " + text + "\n") 78 | } 79 | } 80 | } 81 | 82 | selfName := env.GetRaw("strs.self-name") 83 | PrintTipTitle(screen, env, 84 | selfName+": workflow automating in unix-pipe style") 85 | 86 | pln("") 87 | screen.Print(ColorHelp("usage:\n", env)) 88 | 89 | list := []func(*model.Env) []string{ 90 | GlobalSuggestExeCmds, 91 | GlobalSuggestExeCmdsWithArgs, 92 | GlobalSuggestShowCmdInfo, 93 | GlobalSuggestCmdTree, 94 | GlobalSuggestListCmds, 95 | GlobalSuggestFindCmds, 96 | GlobalSuggestHubAdd, 97 | GlobalSuggestFlowAdd, 98 | GlobalSuggestDesc, 99 | GlobalSuggestSessions, 100 | GlobalSuggestAdvance, 101 | GlobalSuggestShortcut, 102 | GlobalSuggestInteract, 103 | } 104 | 105 | for _, fun := range list { 106 | pln("") 107 | pln(fun(env)...) 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /pkg/cli/display/screen.go: -------------------------------------------------------------------------------- 1 | package display 2 | 3 | import ( 4 | "github.com/innerr/ticat/pkg/core/model" 5 | ) 6 | 7 | func PrintFramedLines(screen model.Screen, env *model.Env, buf *CacheScreen, c *FrameChars) { 8 | if buf.IsEmpty() { 9 | return 10 | } 11 | width := env.GetInt("display.width") - 2 12 | if c == nil { 13 | c = getFrameChars(env) 14 | } 15 | screen.Print(c.P1 + rpt(c.H, width) + c.P3 + "\n") 16 | buf.WriteToEx(screen, func(line string, isError bool, lineLen int) (string, bool) { 17 | rightV := c.V 18 | if lineLen > width { 19 | rightV = "" 20 | } 21 | line = c.V + line + rpt(" ", width-lineLen) + rightV + "\n" 22 | return line, isError 23 | }) 24 | screen.Print(c.P7 + rpt(c.H, width) + c.P9 + "\n") 25 | } 26 | 27 | type CacheScreen struct { 28 | data []CachedOutput 29 | outN int 30 | } 31 | 32 | type CachedOutput struct { 33 | Text string 34 | IsError bool 35 | Len int 36 | } 37 | 38 | func NewCacheScreen() *CacheScreen { 39 | return &CacheScreen{nil, 0} 40 | } 41 | 42 | func (self *CacheScreen) IsEmpty() bool { 43 | return len(self.data) == 0 44 | } 45 | 46 | func (self *CacheScreen) Print(text string) { 47 | self.data = append(self.data, CachedOutput{text, false, len(text)}) 48 | self.outN += 1 49 | } 50 | 51 | func (self *CacheScreen) PrintEx(text string, textLen int) { 52 | self.data = append(self.data, CachedOutput{text, false, textLen}) 53 | self.outN += 1 54 | } 55 | 56 | func (self *CacheScreen) Error(text string) { 57 | self.data = append(self.data, CachedOutput{text, true, len(text)}) 58 | } 59 | 60 | func (self *CacheScreen) OutputtedLines() int { 61 | return self.outN 62 | } 63 | 64 | func (self *CacheScreen) WriteToEx( 65 | screen model.Screen, 66 | transformer func(text string, isError bool, textLen int) (string, bool)) { 67 | 68 | for _, it := range self.data { 69 | text, isError := transformer(it.Text, it.IsError, it.Len) 70 | if isError { 71 | screen.Error(text) 72 | } else { 73 | screen.Print(text) 74 | } 75 | } 76 | } 77 | 78 | func (self *CacheScreen) WriteTo(screen model.Screen) { 79 | self.WriteToEx(screen, func(text string, isError bool, textLen int) (string, bool) { 80 | return text, isError 81 | }) 82 | } 83 | -------------------------------------------------------------------------------- /pkg/cli/display/tags.go: -------------------------------------------------------------------------------- 1 | package display 2 | 3 | import ( 4 | "sort" 5 | 6 | "github.com/innerr/ticat/pkg/core/model" 7 | ) 8 | 9 | // TODO: list cmd usage, now only list the tags 10 | func ListTags( 11 | cmds *model.CmdTree, 12 | screen model.Screen, 13 | env *model.Env) { 14 | 15 | tags := NewTags() 16 | listTags(cmds, screen, env, tags) 17 | 18 | names := tags.Names() 19 | sort.Strings(names) 20 | tagMark := env.GetRaw("strs.tag-mark") 21 | for _, name := range names { 22 | screen.Print(ColorTag(tagMark+name, env) + "\n") 23 | } 24 | } 25 | 26 | type Tags struct { 27 | names []string 28 | set map[string]bool 29 | } 30 | 31 | func NewTags() *Tags { 32 | return &Tags{nil, map[string]bool{}} 33 | } 34 | 35 | func (self *Tags) Add(name string) { 36 | if self.set[name] { 37 | return 38 | } 39 | self.set[name] = true 40 | self.names = append(self.names, name) 41 | } 42 | 43 | func (self *Tags) Names() []string { 44 | return self.names 45 | } 46 | 47 | func listTags( 48 | cmd *model.CmdTree, 49 | screen model.Screen, 50 | env *model.Env, 51 | tags *Tags) { 52 | 53 | for _, tag := range cmd.Tags() { 54 | tags.Add(tag) 55 | } 56 | for _, name := range cmd.SubNames() { 57 | listTags(cmd.GetSub(name), screen, env, tags) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /pkg/cli/display/tip.go: -------------------------------------------------------------------------------- 1 | package display 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/innerr/ticat/pkg/core/model" 8 | ) 9 | 10 | func PrintErrTitle(screen model.Screen, env *model.Env, msgs ...interface{}) { 11 | printTipTitle(screen, env, true, msgs...) 12 | } 13 | 14 | func PrintTipTitle(screen model.Screen, env *model.Env, msgs ...interface{}) { 15 | printTipTitle(screen, env, false, msgs...) 16 | } 17 | 18 | func printTipTitle(screen model.Screen, env *model.Env, isErr bool, msgs ...interface{}) { 19 | var strs []string 20 | for _, it := range msgs { 21 | switch it.(type) { 22 | case string: 23 | strs = append(strs, it.(string)) 24 | case []string: 25 | strs = append(strs, it.([]string)...) 26 | default: 27 | panic(fmt.Errorf("[PrintTipTitle] invalid msg type, should never happen")) 28 | } 29 | } 30 | 31 | printer := NewTipBoxPrinter(screen, env, isErr) 32 | printer.Prints(strs...) 33 | printer.Finish() 34 | } 35 | 36 | type TipBoxPrinter struct { 37 | screen model.Screen 38 | env *model.Env 39 | isErr bool 40 | inited bool 41 | buf *CacheScreen 42 | maxWidth int 43 | } 44 | 45 | func NewTipBoxPrinter(screen model.Screen, env *model.Env, isErr bool) *TipBoxPrinter { 46 | return &TipBoxPrinter{ 47 | screen, 48 | env, 49 | isErr, 50 | false, 51 | NewCacheScreen(), 52 | env.GetInt("display.width") - 4 - 2, 53 | } 54 | } 55 | 56 | func (self *TipBoxPrinter) PrintWrap(msgs ...string) { 57 | for _, msg := range msgs { 58 | for len(msg) > self.maxWidth { 59 | self.Print(msg[0:self.maxWidth]) 60 | msg = msg[self.maxWidth:] 61 | } 62 | self.Print(msg) 63 | } 64 | } 65 | 66 | func (self *TipBoxPrinter) Prints(msgs ...string) { 67 | for _, msg := range msgs { 68 | self.Print(msg) 69 | } 70 | } 71 | 72 | func (self *TipBoxPrinter) colorize(msg string) (string, int) { 73 | if !self.env.GetBool("display.color") { 74 | return msg, 0 75 | } 76 | return "\033[38;5;242m" + msg + "\033[0m", ColorExtraLen(self.env, "tip-dark") 77 | } 78 | 79 | func (self *TipBoxPrinter) Print(msg string) { 80 | msg = strings.TrimRight(msg, "\n") 81 | msgs := strings.Split(msg, "\n") 82 | if len(msgs) > 1 { 83 | self.Prints(msgs...) 84 | return 85 | } 86 | 87 | msg, colorLen := self.colorize(msg) 88 | 89 | // TODO: put ERR TIP to env strs 90 | 91 | if !self.inited { 92 | var tip string 93 | var tipLen int 94 | utf8 := self.env.GetBool("display.utf8.symbols") 95 | if self.isErr { 96 | tip = " " 97 | tipLen = len(tip) 98 | if utf8 { 99 | tip = self.env.GetRaw("display.utf8.symbols.err") 100 | tipLen = self.env.GetInt("display.utf8.symbols.err.len") 101 | } 102 | tip = ColorError(tip, self.env) 103 | } else { 104 | tip = " " 105 | tipLen = len(tip) 106 | if utf8 { 107 | tip = self.env.GetRaw("display.utf8.symbols.tip") 108 | tipLen = self.env.GetInt("display.utf8.symbols.tip.len") 109 | } 110 | tip = ColorTip(tip, self.env) 111 | } 112 | self.buf.PrintEx(tip+msg, len(msg)+tipLen-colorLen) 113 | self.inited = true 114 | } else { 115 | msg = " " + msg 116 | self.buf.PrintEx(msg, len(msg)-colorLen) 117 | } 118 | } 119 | 120 | func (self *TipBoxPrinter) Error(msg string) { 121 | self.buf.Error(msg) 122 | } 123 | 124 | func (self *TipBoxPrinter) OutputtedLines() int { 125 | return self.buf.OutputtedLines() 126 | } 127 | 128 | func (self *TipBoxPrinter) Finish() { 129 | if !self.env.GetBool("display.tip") { 130 | return 131 | } 132 | colorCode := colorCodeTipDark 133 | if self.isErr { 134 | colorCode = colorCodeError 135 | } 136 | var frameChars *FrameChars 137 | if self.env.GetBool("display.utf8") { 138 | if self.env.GetBool("display.color") { 139 | frameChars = FrameCharsUtf8Colored(colorCode) 140 | } else { 141 | frameChars = FrameCharsUtf8() 142 | } 143 | } else { 144 | if self.env.GetBool("display.color") { 145 | frameChars = FrameCharsAsciiColored(colorCode) 146 | } else { 147 | frameChars = FrameCharsAscii() 148 | } 149 | } 150 | PrintFramedLines(self.screen, self.env, self.buf, frameChars) 151 | } 152 | -------------------------------------------------------------------------------- /pkg/cli/display/utils.go: -------------------------------------------------------------------------------- 1 | package display 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | "time" 7 | 8 | "github.com/innerr/ticat/pkg/core/model" 9 | ) 10 | 11 | func TooMuchOutput(env *model.Env, screen model.Screen) bool { 12 | height := env.GetInt("display.height") 13 | return height > 0 && screen.OutputtedLines() > int(float64(height)*1.1) 14 | } 15 | 16 | func mayQuoteStr(origin string) string { 17 | trimed := strings.TrimSpace(origin) 18 | if len(trimed) == 0 || len(trimed) != len(origin) { 19 | return "'" + origin + "'" 20 | } 21 | fields := strings.Fields(origin) 22 | if len(fields) != 1 { 23 | return "'" + origin + "'" 24 | } 25 | return origin 26 | } 27 | 28 | func autoPadNewLine(padding string, msg string) string { 29 | msgNoPad := strings.TrimLeft(msg, "\t '\"") 30 | hiddenPad := rpt(" ", len(msg)-len(msgNoPad)) 31 | msg = strings.ReplaceAll(msg, "\n", "\n"+padding+hiddenPad) 32 | return msg 33 | } 34 | 35 | func padCmd(str string, width int, env *model.Env) string { 36 | return ColorCmd(padR(str, width), env) 37 | } 38 | 39 | func padR(str string, width int) string { 40 | return padRight(str, " ", width) 41 | } 42 | 43 | func padRight(str string, pad string, width int) string { 44 | if len(str) >= width { 45 | return str 46 | } 47 | return str + strings.Repeat(pad, width-len(str)) 48 | } 49 | 50 | func formatDuration(dur time.Duration) string { 51 | return strings.ReplaceAll(fmt.Sprintf("%s", dur), "µ", "u") 52 | } 53 | 54 | func rpt(char string, count int) string { 55 | if count <= 0 { 56 | return "" 57 | } 58 | return strings.Repeat(char, count) 59 | } 60 | 61 | func mayMaskSensitiveVal(env *model.Env, key string, val string) string { 62 | if env.GetBool("display.sensitive") { 63 | return val 64 | } 65 | if model.IsSensitiveKeyVal(key, val) && len(val) != 0 { 66 | val = "***" 67 | } 68 | return val 69 | } 70 | -------------------------------------------------------------------------------- /pkg/core/execute/cmd_type.go: -------------------------------------------------------------------------------- 1 | package execute 2 | 3 | import ( 4 | "github.com/innerr/ticat/pkg/core/model" 5 | "github.com/innerr/ticat/pkg/mods/builtin" 6 | ) 7 | 8 | // TODO: move to command property 9 | func allowCheckEnvOpsFail(flow *model.ParsedCmds) bool { 10 | last := flow.Cmds[0].LastCmd() 11 | if last == nil { 12 | return false 13 | } 14 | // This list equals to funcs which will do 'clear-the-flow' 15 | allows := []interface{}{ 16 | builtin.SaveFlow, 17 | builtin.GlobalFindCmds, 18 | builtin.GlobalFindCmdsWithUsage, 19 | builtin.GlobalFindCmdsWithDetails, 20 | builtin.DumpTailCmdWithUsage, 21 | builtin.DumpTailCmdWithDetails, 22 | //builtin.DumpTailCmdSub, 23 | //builtin.DumpTailCmdSubWithUsage, 24 | //builtin.DumpTailCmdSubWithDetails, 25 | builtin.DumpFlowAll, 26 | builtin.DumpFlowAllSimple, 27 | builtin.DumpFlow, 28 | builtin.DumpFlowSimple, 29 | builtin.DumpFlowDepends, 30 | builtin.DumpFlowSkeleton, 31 | builtin.DumpFlowEnvOpsCheckResult, 32 | builtin.DumpCmds, 33 | builtin.DumpCmdsWithUsage, 34 | builtin.DumpCmdsWithDetails, 35 | } 36 | for _, allow := range allows { 37 | if last.IsTheSameFunc(allow) { 38 | return true 39 | } 40 | } 41 | 42 | return false 43 | } 44 | 45 | // TODO: try to remove this, it just for better display 46 | func isStartWithSearchCmd(flow *model.ParsedCmds) (isSearch bool) { 47 | if len(flow.Cmds) == 0 { 48 | return 49 | } 50 | last := flow.Cmds[0].LastCmd() 51 | if last == nil { 52 | return 53 | } 54 | funcs := []interface{}{ 55 | builtin.GlobalFindCmds, 56 | builtin.GlobalFindCmdsWithUsage, 57 | builtin.GlobalFindCmdsWithDetails, 58 | builtin.DumpCmds, 59 | builtin.DumpCmdsWithUsage, 60 | builtin.DumpCmdsWithDetails, 61 | } 62 | for _, it := range funcs { 63 | if last.IsTheSameFunc(it) { 64 | return true 65 | } 66 | } 67 | return false 68 | } 69 | 70 | func allowParseError(flow *model.ParsedCmds) bool { 71 | if len(flow.Cmds) == 0 { 72 | return false 73 | } 74 | last := flow.Cmds[0].LastCmd() 75 | if last == nil { 76 | return false 77 | } 78 | if flow.TailModeCall { 79 | return flow.Cmds[0].ParseResult.Error == nil 80 | } 81 | funcs := []interface{}{ 82 | builtin.SaveFlow, 83 | } 84 | for _, it := range funcs { 85 | if last.IsTheSameFunc(it) { 86 | return true 87 | } 88 | } 89 | return false 90 | } 91 | 92 | func noSessionCmds(flow *model.ParsedCmds) bool { 93 | if len(flow.Cmds) == 0 { 94 | return true 95 | } 96 | cmd := flow.Cmds[0].LastCmdNode() 97 | 98 | if cmd != nil && cmd.Cmd() != nil && cmd.Cmd().IsNoSessionCmd() { 99 | return true 100 | } 101 | 102 | if flow.HasTailMode { 103 | funcs := []interface{}{ 104 | builtin.DbgBreakAtEnd, 105 | } 106 | ignore := false 107 | for _, it := range funcs { 108 | if cmd.Cmd() != nil && cmd.Cmd().IsTheSameFunc(it) { 109 | ignore = true 110 | break 111 | } 112 | } 113 | if !ignore { 114 | return true 115 | } 116 | } 117 | 118 | if len(flow.Cmds) != 1 { 119 | return false 120 | } 121 | 122 | if cmd == nil { 123 | return true 124 | } 125 | 126 | if !cmd.IsBuiltin() { 127 | return false 128 | } 129 | 130 | funcs := []interface{}{ 131 | builtin.DbgInteract, 132 | builtin.SessionRetry, 133 | builtin.Selftest, 134 | builtin.Repeat, 135 | builtin.LastSessionRetry, 136 | builtin.LastErrorSessionRetry, 137 | } 138 | for _, it := range funcs { 139 | if cmd.Cmd() != nil && cmd.Cmd().IsTheSameFunc(it) { 140 | return false 141 | } 142 | } 143 | return true 144 | } 145 | -------------------------------------------------------------------------------- /pkg/core/model/args.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | ) 7 | 8 | type Args struct { 9 | // map[arg-name]arg-index 10 | names map[string]int 11 | 12 | // Not support no-default-value arg yet, so names could be insteaded by defVals now 13 | defVals map[string]string 14 | 15 | orderedList []string 16 | abbrs map[string][]string 17 | abbrsRevIdx map[string]string 18 | 19 | fromAutoMapAll map[string]bool 20 | 21 | // map[arg-name]enum-values 22 | enums map[string][]string 23 | } 24 | 25 | func newArgs() Args { 26 | return Args{ 27 | map[string]int{}, 28 | map[string]string{}, 29 | []string{}, 30 | map[string][]string{}, 31 | map[string]string{}, 32 | map[string]bool{}, 33 | map[string][]string{}, 34 | } 35 | } 36 | 37 | func (self *Args) IsEmpty() bool { 38 | return len(self.names) == 0 39 | } 40 | 41 | func (self *Args) AddArg(owner *CmdTree, name string, defVal string, abbrs ...string) { 42 | if _, ok := self.names[name]; ok { 43 | panic(fmt.Errorf("[Args.AddArg] %s: arg name conflicted: %s", 44 | owner.DisplayPath(), name)) 45 | } 46 | abbrs = append([]string{name}, abbrs...) 47 | for _, abbr := range abbrs { 48 | if len(abbr) == 0 { 49 | continue 50 | } 51 | if old, ok := self.abbrsRevIdx[abbr]; ok { 52 | panic(fmt.Errorf("[Args.AddArg] %s: arg abbr name '%s' conflicted,"+ 53 | " old for '%s', new for '%s'", 54 | owner.DisplayPath(), abbr, old, name)) 55 | } 56 | self.abbrsRevIdx[abbr] = name 57 | } 58 | self.abbrs[name] = abbrs 59 | self.names[name] = len(self.names) 60 | self.defVals[name] = defVal 61 | self.orderedList = append(self.orderedList, name) 62 | } 63 | 64 | func (self *Args) SetArgEnums(owner *CmdTree, name string, vals ...string) { 65 | if _, ok := self.names[name]; !ok { 66 | panic(fmt.Errorf("[Args.AddArg] %s: arg name not exists: %s", 67 | owner.DisplayPath(), name)) 68 | } 69 | if _, ok := self.enums[name]; ok { 70 | panic(fmt.Errorf("[Args.AddArg] %s: arg's enum list already exists: %s", 71 | owner.DisplayPath(), strings.Join(vals, owner.Strs.ArgEnumSep))) 72 | } 73 | self.enums[name] = vals 74 | } 75 | 76 | func (self *Args) EnumVals(name string) []string { 77 | return self.enums[name] 78 | } 79 | 80 | func (self *Args) AddAutoMapAllArg(owner *CmdTree, name string, defVal string, abbrs ...string) { 81 | self.AddArg(owner, name, defVal, abbrs...) 82 | self.fromAutoMapAll[name] = true 83 | } 84 | 85 | func (self *Args) IsFromAutoMapAll(name string) bool { 86 | return self.fromAutoMapAll[name] 87 | } 88 | 89 | func (self Args) MatchFind(findStr string) bool { 90 | for k, _ := range self.abbrsRevIdx { 91 | if strings.Index(k, findStr) >= 0 { 92 | return true 93 | } 94 | } 95 | return false 96 | } 97 | 98 | func (self *Args) Names() []string { 99 | return self.orderedList 100 | } 101 | 102 | func (self *Args) Reorder(owner *Cmd, names []string) { 103 | changed := func() { 104 | panic(fmt.Errorf("[%s] args changed in reordering, origin: %s; new: %s", 105 | owner.Owner().DisplayPath(), strings.Join(self.orderedList, ","), strings.Join(names, ","))) 106 | } 107 | if len(names) != len(self.orderedList) { 108 | changed() 109 | } 110 | for i, name := range names { 111 | if _, ok := self.names[name]; !ok { 112 | changed() 113 | } 114 | self.names[name] = i 115 | } 116 | self.orderedList = names 117 | } 118 | 119 | func (self *Args) DefVal(name string, stackDepth int) string { 120 | if self.fromAutoMapAll[name] && stackDepth > 1 { 121 | return "" 122 | } 123 | return self.defVals[name] 124 | } 125 | 126 | func (self *Args) RawDefVal(name string) string { 127 | return self.defVals[name] 128 | } 129 | 130 | func (self *Args) Realname(nameOrAbbr string) string { 131 | name, _ := self.abbrsRevIdx[nameOrAbbr] 132 | return name 133 | } 134 | 135 | func (self *Args) Abbrs(name string) (abbrs []string) { 136 | abbrs, _ = self.abbrs[name] 137 | return 138 | } 139 | 140 | func (self *Args) Has(name string) bool { 141 | _, ok := self.names[name] 142 | return ok 143 | } 144 | 145 | func (self *Args) HasArgOrAbbr(nameOrAbbr string) bool { 146 | _, ok := self.abbrsRevIdx[nameOrAbbr] 147 | return ok 148 | } 149 | 150 | func (self *Args) Index(name string) int { 151 | index, ok := self.names[name] 152 | if !ok { 153 | return -1 154 | } 155 | return index 156 | } 157 | -------------------------------------------------------------------------------- /pkg/core/model/argv.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | ) 7 | 8 | type ArgVals map[string]ArgVal 9 | 10 | type ArgVal struct { 11 | Raw string 12 | Provided bool 13 | Index int 14 | } 15 | 16 | func (self ArgVals) GetRaw(name string) (raw string) { 17 | val, ok := self[name] 18 | if !ok { 19 | return 20 | } 21 | return val.Raw 22 | } 23 | 24 | func (self ArgVals) GetRawEx(name string, defVal string) (raw string) { 25 | val, ok := self[name] 26 | if !ok { 27 | return defVal 28 | } 29 | return val.Raw 30 | } 31 | 32 | func (self ArgVals) GetUint64(name string) uint64 { 33 | val, ok := self[name] 34 | if !ok { 35 | panic(&ArgValErrNotFound{ 36 | fmt.Sprintf("[ArgVals.GetUint64] arg '%s' not found", name), 37 | name, 38 | }) 39 | } 40 | if len(val.Raw) == 0 { 41 | return 0 42 | } 43 | intVal, err := strconv.ParseUint(val.Raw, 10, 64) 44 | if err != nil { 45 | panic(&ArgValErrWrongType{ 46 | fmt.Sprintf("[ArgVals.GetUint64] arg '%s' = '%s' is not uint64: %v", name, val.Raw, err), 47 | name, val.Raw, "uint64", err, 48 | }) 49 | } 50 | return uint64(intVal) 51 | } 52 | 53 | func (self ArgVals) GetUint64Ex(name string, defVal uint64) uint64 { 54 | _, ok := self[name] 55 | if !ok { 56 | return defVal 57 | } 58 | return self.GetUint64(name) 59 | } 60 | 61 | func (self ArgVals) GetInt(name string) int { 62 | val, ok := self[name] 63 | if !ok { 64 | panic(&ArgValErrNotFound{ 65 | fmt.Sprintf("[ArgVals.GetInt] arg '%s' not found", name), 66 | name, 67 | }) 68 | } 69 | if len(val.Raw) == 0 { 70 | return 0 71 | } 72 | intVal, err := strconv.Atoi(val.Raw) 73 | if err != nil { 74 | panic(&ArgValErrWrongType{ 75 | fmt.Sprintf("[ArgVals.GetInt] arg '%s' = '%s' is not int: %v", name, val.Raw, err), 76 | name, val.Raw, "int", err, 77 | }) 78 | } 79 | return int(intVal) 80 | } 81 | 82 | func (self ArgVals) GetIntEx(name string, defVal int) int { 83 | _, ok := self[name] 84 | if !ok { 85 | return defVal 86 | } 87 | return self.GetInt(name) 88 | } 89 | 90 | func (self ArgVals) GetBool(name string) bool { 91 | val, ok := self[name] 92 | if !ok { 93 | panic(&ArgValErrNotFound{ 94 | fmt.Sprintf("[ArgVals.GetBool] arg '%s' not found", name), 95 | name, 96 | }) 97 | } 98 | return StrToBool(val.Raw) 99 | } 100 | 101 | func (self ArgVals) GetBoolEx(name string, defVal bool) bool { 102 | val, ok := self[name] 103 | if !ok { 104 | return defVal 105 | } 106 | return StrToBool(val.Raw) 107 | } 108 | 109 | type ArgValErrNotFound struct { 110 | Str string 111 | ArgName string 112 | } 113 | 114 | func (self ArgValErrNotFound) Error() string { 115 | return self.Str 116 | } 117 | 118 | type ArgValErrWrongType struct { 119 | Str string 120 | ArgName string 121 | Val string 122 | ExpectType string 123 | ConvertErr error 124 | } 125 | 126 | func (self ArgValErrWrongType) Error() string { 127 | return self.Str 128 | } 129 | -------------------------------------------------------------------------------- /pkg/core/model/breakpoints.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "sort" 5 | ) 6 | 7 | type BreakPoints struct { 8 | AtEnd bool 9 | Befores map[string]bool 10 | Afters map[string]bool 11 | } 12 | 13 | func NewBreakPoints() *BreakPoints { 14 | return &BreakPoints{false, map[string]bool{}, map[string]bool{}} 15 | } 16 | 17 | func (self *BreakPoints) SetAtEnd(enabled bool) { 18 | self.AtEnd = enabled 19 | } 20 | 21 | func (self *BreakPoints) BreakAtEnd() bool { 22 | return self.AtEnd 23 | } 24 | 25 | func (self *BreakPoints) SetBefores(cc *Cli, env *Env, cmdList []string) (verifiedCmds []string) { 26 | for _, cmd := range cmdList { 27 | verifiedCmd := cc.NormalizeCmd(true, cmd) 28 | verifiedCmds = append(verifiedCmds, verifiedCmd) 29 | self.Befores[verifiedCmd] = true 30 | } 31 | sort.Strings(verifiedCmds) 32 | return 33 | } 34 | 35 | func (self *BreakPoints) SetAfters(cc *Cli, env *Env, cmdList []string) (verifiedCmds []string) { 36 | for _, cmd := range cmdList { 37 | verifiedCmd := cc.NormalizeCmd(true, cmd) 38 | verifiedCmds = append(verifiedCmds, verifiedCmd) 39 | self.Afters[verifiedCmd] = true 40 | } 41 | sort.Strings(verifiedCmds) 42 | return 43 | } 44 | 45 | func (self *BreakPoints) IsEmpty() bool { 46 | return self.AtEnd == false && len(self.Befores) == 0 && len(self.Afters) == 0 47 | } 48 | 49 | func (self *BreakPoints) Clean(cc *Cli, env *Env) { 50 | self.AtEnd = false 51 | self.Befores = map[string]bool{} 52 | self.Afters = map[string]bool{} 53 | } 54 | 55 | func (self *BreakPoints) BreakBefore(cmd string) bool { 56 | return self.Befores[cmd] 57 | } 58 | 59 | func (self *BreakPoints) BreakAfter(cmd string) bool { 60 | return self.Afters[cmd] 61 | } 62 | -------------------------------------------------------------------------------- /pkg/core/model/cli.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | ) 7 | 8 | type Screen interface { 9 | Print(text string) 10 | Error(text string) 11 | // Same as line-number, but it's the count of 'Print' 12 | OutputtedLines() int 13 | } 14 | 15 | type ExecPolicy string 16 | 17 | const ExecPolicyExec ExecPolicy = "exec" 18 | const ExecPolicySkip ExecPolicy = "skip" 19 | 20 | type ExecuteMask struct { 21 | Cmd string 22 | OverWriteStartEnv *Env 23 | OverWriteFinishEnv *Env 24 | ExecPolicy ExecPolicy 25 | FileNFlowExecPolicy ExecPolicy 26 | SubFlow []*ExecuteMask 27 | ResultIfExecuted ExecutedResult 28 | ExecutedCmd *ExecutedCmd 29 | } 30 | 31 | func NewExecuteMask(cmd string) *ExecuteMask { 32 | return &ExecuteMask{cmd, nil, nil, ExecPolicyExec, ExecPolicyExec, nil, ExecutedResultUnRun, nil} 33 | } 34 | 35 | func (self *ExecuteMask) Copy() *ExecuteMask { 36 | return &ExecuteMask{ 37 | self.Cmd, 38 | self.OverWriteStartEnv, 39 | self.OverWriteFinishEnv, 40 | self.ExecPolicy, 41 | self.FileNFlowExecPolicy, 42 | self.SubFlow, 43 | ExecutedResultUnRun, 44 | self.ExecutedCmd, 45 | } 46 | } 47 | 48 | func (self *ExecuteMask) SetExecPolicyForAll(policy ExecPolicy) { 49 | self.ExecPolicy = policy 50 | self.FileNFlowExecPolicy = policy 51 | for _, sub := range self.SubFlow { 52 | sub.SetExecPolicyForAll(policy) 53 | } 54 | } 55 | 56 | type Executor interface { 57 | Execute(caller string, inerrCall bool, cc *Cli, env *Env, masks []*ExecuteMask, input ...string) bool 58 | Clone() Executor 59 | } 60 | 61 | type HandledErrors map[interface{}]bool 62 | 63 | type Cli struct { 64 | Screen Screen 65 | Cmds *CmdTree 66 | Parser CliParser 67 | EnvAbbrs *EnvAbbrs 68 | TolerableErrs *TolerableErrs 69 | Executor Executor 70 | Helps *Helps 71 | BgTasks *BgTasks 72 | CmdIO *CmdIO 73 | FlowStatus *ExecutingFlow 74 | BreakPoints *BreakPoints 75 | HandledErrors HandledErrors 76 | ForestMode *ForestMode 77 | Blender *Blender 78 | Arg2EnvAutoMapCmds Arg2EnvAutoMapCmds 79 | EnvKeysInfo *EnvKeysInfo 80 | } 81 | 82 | func NewCli(screen Screen, cmds *CmdTree, parser CliParser, abbrs *EnvAbbrs, cmdIO *CmdIO, 83 | envKeysInfo *EnvKeysInfo) *Cli { 84 | 85 | return &Cli{ 86 | screen, 87 | cmds, 88 | parser, 89 | abbrs, 90 | NewTolerableErrs(), 91 | nil, 92 | NewHelps(), 93 | NewBgTasks(), 94 | cmdIO, 95 | nil, 96 | NewBreakPoints(), 97 | HandledErrors{}, 98 | NewForestMode(), 99 | NewBlender(), 100 | Arg2EnvAutoMapCmds{}, 101 | envKeysInfo, 102 | } 103 | } 104 | 105 | func (self *Cli) SetFlowStatusWriter(status *ExecutingFlow) { 106 | if self.FlowStatus != nil { 107 | panic(fmt.Errorf("[SetFlowStatusWriter] should never happen")) 108 | } 109 | self.FlowStatus = status 110 | } 111 | 112 | // Shadow copy 113 | func (self *Cli) CopyForInteract() *Cli { 114 | return &Cli{ 115 | self.Screen, 116 | self.Cmds, 117 | self.Parser, 118 | self.EnvAbbrs, 119 | self.TolerableErrs, 120 | self.Executor, 121 | self.Helps, 122 | self.BgTasks, 123 | self.CmdIO, 124 | nil, 125 | self.BreakPoints, 126 | HandledErrors{}, 127 | self.ForestMode, 128 | self.Blender, 129 | Arg2EnvAutoMapCmds{}, 130 | self.EnvKeysInfo, 131 | } 132 | } 133 | 134 | // TODO: fixme, do real clone for ready-only instances 135 | func (self *Cli) CloneForAsyncExecuting(env *Env) *Cli { 136 | screen := NewBgTaskScreen() 137 | bgStdout := screen.GetBgStdout() 138 | return &Cli{ 139 | screen, 140 | self.Cmds, 141 | self.Parser, 142 | self.EnvAbbrs, 143 | NewTolerableErrs(), 144 | self.Executor.Clone(), 145 | self.Helps, 146 | self.BgTasks, 147 | NewCmdIO(nil, bgStdout, bgStdout), 148 | nil, 149 | NewBreakPoints(), 150 | HandledErrors{}, 151 | self.ForestMode.Clone(), 152 | self.Blender.Clone(), 153 | Arg2EnvAutoMapCmds{}, 154 | self.EnvKeysInfo, 155 | } 156 | } 157 | 158 | func (self *Cli) CloneForChecking() *Cli { 159 | return &Cli{ 160 | self.Screen, 161 | self.Cmds, 162 | self.Parser, 163 | self.EnvAbbrs, 164 | self.TolerableErrs, 165 | self.Executor, 166 | self.Helps, 167 | self.BgTasks, 168 | self.CmdIO, 169 | self.FlowStatus, 170 | self.BreakPoints, 171 | self.HandledErrors, 172 | self.ForestMode, 173 | self.Blender.Clone(), 174 | Arg2EnvAutoMapCmds{}, 175 | self.EnvKeysInfo, 176 | } 177 | } 178 | 179 | func (self *Cli) ParseCmd(panicOnError bool, cmdAndArgs ...string) (parsedCmd ParsedCmd, ok bool) { 180 | parsed := self.Parser.Parse(self.Cmds, self.EnvAbbrs, cmdAndArgs...) 181 | err := parsed.FirstErr() 182 | if len(parsed.Cmds) != 1 || err != nil { 183 | if panicOnError { 184 | cmdStr := strings.Join(cmdAndArgs, " ") 185 | if err != nil { 186 | panic(err.Error) 187 | } 188 | if len(parsed.Cmds) > 1 { 189 | panic(fmt.Errorf("[Cli.ParseCmd] too many result commands by parsing '%s': %d", 190 | cmdStr, len(parsed.Cmds))) 191 | } else { 192 | panic(fmt.Errorf("[Cli.ParseCmd] no result command by parsing '%s'", cmdStr)) 193 | } 194 | } else { 195 | return 196 | } 197 | } 198 | return parsed.Cmds[0], true 199 | } 200 | 201 | func (self *Cli) NormalizeCmd(panicOnError bool, cmd string) (verifiedCmdPath string) { 202 | parsed, ok := self.ParseCmd(panicOnError, cmd) 203 | if ok { 204 | verifiedCmdPath = strings.Join(parsed.Path(), self.Cmds.Strs.PathSep) 205 | } 206 | return 207 | } 208 | -------------------------------------------------------------------------------- /pkg/core/model/dep.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | type DependInfo struct { 4 | Reason string 5 | Cmd ParsedCmd 6 | } 7 | 8 | type Depends map[string]map[*Cmd]DependInfo 9 | 10 | func CollectDepends( 11 | cc *Cli, 12 | env *Env, 13 | flow *ParsedCmds, 14 | currCmdIdx int, 15 | res Depends, 16 | allowFlowTemplateRenderError bool, 17 | envOpCmds []EnvOpCmd) { 18 | 19 | collectDepends(cc, env.Clone(), flow, currCmdIdx, res, allowFlowTemplateRenderError, envOpCmds, 0) 20 | } 21 | 22 | func collectDepends( 23 | cc *Cli, 24 | env *Env, 25 | flow *ParsedCmds, 26 | currCmdIdx int, 27 | res Depends, 28 | allowFlowTemplateRenderError bool, 29 | envOpCmds []EnvOpCmd, 30 | depth int) { 31 | 32 | for i := currCmdIdx; i < len(flow.Cmds); i++ { 33 | it := flow.Cmds[i] 34 | cic := it.LastCmd() 35 | if cic == nil { 36 | continue 37 | } 38 | 39 | cmdEnv, argv := it.ApplyMappingGenEnvAndArgv(env, cc.Cmds.Strs.EnvValDelAllMark, cc.Cmds.Strs.PathSep, depth+1) 40 | 41 | if cic.Type() == CmdTypeFileNFlow { 42 | subFlow, _, rendered := cic.Flow(argv, cc, cmdEnv, allowFlowTemplateRenderError, true) 43 | if rendered && len(subFlow) != 0 { 44 | parsedFlow := cc.Parser.Parse(cc.Cmds, cc.EnvAbbrs, subFlow...) 45 | flowEnv := cmdEnv.NewLayer(EnvLayerSubFlow) 46 | parsedFlow.GlobalEnv.WriteNotArgTo(flowEnv, cc.Cmds.Strs.EnvValDelAllMark) 47 | // Allow parse errors here 48 | collectDepends(cc, flowEnv, parsedFlow, 0, res, allowFlowTemplateRenderError, envOpCmds, depth+1) 49 | } 50 | } 51 | 52 | deps := cic.GetDepends() 53 | for _, dep := range deps { 54 | cmds, ok := res[dep.OsCmd] 55 | if ok { 56 | cmds[cic] = DependInfo{dep.Reason, it} 57 | } else { 58 | res[dep.OsCmd] = map[*Cmd]DependInfo{cic: DependInfo{dep.Reason, it}} 59 | } 60 | } 61 | 62 | TryExeEnvOpCmds(argv, cc, cmdEnv, flow, i, envOpCmds, nil, 63 | "failed to execute env op-cmd in depends collecting") 64 | 65 | if !cic.HasSubFlow(true) { 66 | continue 67 | } 68 | 69 | subFlow, _, rendered := cic.Flow(argv, cc, cmdEnv, allowFlowTemplateRenderError, true) 70 | if rendered && len(subFlow) != 0 { 71 | parsedFlow := cc.Parser.Parse(cc.Cmds, cc.EnvAbbrs, subFlow...) 72 | flowEnv := cmdEnv.NewLayer(EnvLayerSubFlow) 73 | parsedFlow.GlobalEnv.WriteNotArgTo(flowEnv, cc.Cmds.Strs.EnvValDelAllMark) 74 | // Allow parse errors here 75 | collectDepends(cc, flowEnv, parsedFlow, 0, res, allowFlowTemplateRenderError, envOpCmds, depth+1) 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /pkg/core/model/env_abbr.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | ) 7 | 8 | type EnvAbbrs struct { 9 | rootDisplayName string 10 | 11 | name string 12 | parent *EnvAbbrs 13 | subs map[string]*EnvAbbrs 14 | subOrderedNames []string 15 | subAbbrs map[string][]string 16 | subAbbrsRevIdx map[string]string 17 | } 18 | 19 | func NewEnvAbbrs(rootDisplayName string) *EnvAbbrs { 20 | return &EnvAbbrs{ 21 | rootDisplayName, 22 | "", 23 | nil, 24 | map[string]*EnvAbbrs{}, 25 | nil, 26 | map[string][]string{}, 27 | map[string]string{}, 28 | } 29 | } 30 | 31 | func (self *EnvAbbrs) AddSub(name string, abbrs ...string) *EnvAbbrs { 32 | if old, ok := self.subs[name]; ok && old.name != name { 33 | panic(fmt.Errorf("[EnvAbbrs.AddSub] %s: sub-node name conflicted: %s", 34 | self.DisplayPath(), name)) 35 | } 36 | sub := NewEnvAbbrs(self.rootDisplayName) 37 | sub.name = name 38 | sub.parent = self 39 | self.subs[name] = sub 40 | self.subOrderedNames = append(self.subOrderedNames, name) 41 | self.AddSubAbbrs(name, abbrs...) 42 | self.subAbbrsRevIdx[name] = name 43 | return sub 44 | } 45 | 46 | func (self *EnvAbbrs) AddSubAbbrs(name string, abbrs ...string) { 47 | for _, abbr := range append([]string{name}, abbrs...) { 48 | if len(abbr) == 0 { 49 | continue 50 | } 51 | old, ok := self.subAbbrsRevIdx[abbr] 52 | if old == name { 53 | continue 54 | } 55 | if ok { 56 | panic(fmt.Errorf( 57 | "[EnvAbbrs.AddSubAbbrs] %s: command abbr name '%s' conflicted, "+ 58 | "old for '%s', new for '%s'", 59 | self.DisplayPath(), abbr, old, name)) 60 | } 61 | self.subAbbrsRevIdx[abbr] = name 62 | olds, _ := self.subAbbrs[name] 63 | self.subAbbrs[name] = append(olds, abbr) 64 | } 65 | } 66 | 67 | func (self *EnvAbbrs) AddAbbrs(abbrs ...string) *EnvAbbrs { 68 | if self.parent == nil { 69 | panic(fmt.Errorf("[EnvAbbrs.AddAbbr] can't add abbrs %v to root", abbrs)) 70 | } 71 | self.parent.AddSubAbbrs(self.name, abbrs...) 72 | return self 73 | } 74 | 75 | // TODO: move to parser dir 76 | func (self *EnvAbbrs) TryMatch(path string, sep string) (matchedPath []string, matched bool) { 77 | for len(path) > 0 { 78 | i := strings.Index(path, sep) 79 | if i == 0 { 80 | path = path[1:] 81 | continue 82 | } 83 | var candidate string 84 | if i > 0 { 85 | candidate = path[0:i] 86 | path = path[i+1:] 87 | } else { 88 | candidate = path 89 | path = "" 90 | } 91 | subName, ok := self.subAbbrsRevIdx[candidate] 92 | if !ok { 93 | matched = false 94 | return 95 | } 96 | matchedPath = append(matchedPath, subName) 97 | sub, _ := self.subs[subName] 98 | var subMatchedPath []string 99 | subMatchedPath, matched = sub.TryMatch(path, sep) 100 | if len(subMatchedPath) != 0 { 101 | matchedPath = append(matchedPath, subMatchedPath...) 102 | } 103 | return 104 | } 105 | matched = true 106 | return 107 | } 108 | 109 | func (self *EnvAbbrs) GetOrAddSub(name string) *EnvAbbrs { 110 | sub, _ := self.subs[name] 111 | if sub == nil { 112 | sub = self.AddSub(name) 113 | } 114 | return sub 115 | } 116 | 117 | func (self *EnvAbbrs) GetSub(name string) *EnvAbbrs { 118 | sub, _ := self.subs[name] 119 | return sub 120 | } 121 | 122 | func (self *EnvAbbrs) Path() []string { 123 | if self.parent == nil { 124 | return []string{} 125 | } 126 | return append(self.parent.Path(), self.name) 127 | } 128 | 129 | func (self *EnvAbbrs) DisplayPath() string { 130 | path := self.Path() 131 | if len(path) == 0 { 132 | return self.rootDisplayName 133 | } else { 134 | return strings.Join(path, ".") 135 | } 136 | } 137 | 138 | func (self *EnvAbbrs) SubNames() []string { 139 | return self.subOrderedNames 140 | } 141 | 142 | func (self *EnvAbbrs) SubAbbrs(name string) (abbrs []string) { 143 | abbrs, _ = self.subAbbrs[name] 144 | return 145 | } 146 | 147 | func (self *EnvAbbrs) Abbrs() (abbrs []string) { 148 | if self.parent == nil { 149 | return []string{self.DisplayName()} 150 | } 151 | return self.parent.SubAbbrs(self.name) 152 | } 153 | 154 | func (self *EnvAbbrs) Name() string { 155 | return self.name 156 | } 157 | 158 | func (self *EnvAbbrs) DisplayName() string { 159 | if len(self.name) == 0 { 160 | return self.rootDisplayName 161 | } 162 | return self.name 163 | } 164 | -------------------------------------------------------------------------------- /pkg/core/model/env_info.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | type EnvKeyInfo struct { 4 | DisplayLen int 5 | InvisibleDisplay string 6 | } 7 | 8 | type EnvKeysInfo map[string]*EnvKeyInfo 9 | 10 | func NewEnvKeysInfo() *EnvKeysInfo { 11 | return &EnvKeysInfo{} 12 | } 13 | 14 | func (self *EnvKeysInfo) GetOrAdd(key string) *EnvKeyInfo { 15 | val, ok := (*self)[key] 16 | if !ok { 17 | val = &EnvKeyInfo{} 18 | (*self)[key] = val 19 | } 20 | return val 21 | } 22 | 23 | func (self *EnvKeysInfo) Get(key string) *EnvKeyInfo { 24 | return (*self)[key] 25 | } 26 | -------------------------------------------------------------------------------- /pkg/core/model/env_mapper.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | ) 7 | 8 | type Val2Env struct { 9 | orderedKeys []string 10 | pairs map[string]string 11 | } 12 | 13 | func newVal2Env() *Val2Env { 14 | return &Val2Env{nil, map[string]string{}} 15 | } 16 | 17 | func (self *Val2Env) IsEmpty() bool { 18 | return len(self.orderedKeys) == 0 19 | } 20 | 21 | func (self *Val2Env) Add(envKey string, val string) { 22 | _, ok := self.pairs[envKey] 23 | if ok { 24 | panic(fmt.Errorf("[Val2Env.Add] duplicated key: %s", envKey)) 25 | } 26 | self.orderedKeys = append(self.orderedKeys, envKey) 27 | self.pairs[envKey] = val 28 | } 29 | 30 | func (self *Val2Env) EnvKeys() []string { 31 | return self.orderedKeys 32 | } 33 | 34 | func (self *Val2Env) RenderedEnvKeys( 35 | argv ArgVals, 36 | env *Env, 37 | cmd *Cmd, 38 | allowError bool) (renderedKeys []string, origins []string, fullyRendered bool) { 39 | 40 | fullyRendered = true 41 | for _, name := range self.orderedKeys { 42 | keys, keyFullyRendered := renderTemplateStr(name, "map arg to env", cmd, argv, env, allowError) 43 | for _, key := range keys { 44 | renderedKeys = append(renderedKeys, key) 45 | origins = append(origins, name) 46 | } 47 | fullyRendered = fullyRendered && keyFullyRendered 48 | } 49 | return 50 | } 51 | 52 | func (self *Val2Env) Val(envKey string) string { 53 | return self.pairs[envKey] 54 | } 55 | 56 | func (self *Val2Env) RenderedVal( 57 | envKey string, 58 | argv ArgVals, 59 | env *Env, 60 | cmd *Cmd, 61 | allowError bool) string { 62 | 63 | val := self.pairs[envKey] 64 | lines, _ := renderTemplateStr(val, "render val in val2env", cmd, argv, env, allowError) 65 | return strings.Join(lines, "\n") 66 | } 67 | 68 | func (self *Val2Env) Has(envKey string) bool { 69 | _, ok := self.pairs[envKey] 70 | return ok 71 | } 72 | 73 | func (self *Val2Env) MatchFind(findStr string) bool { 74 | if strings.Index("write", findStr) >= 0 { 75 | return true 76 | } 77 | for _, envKey := range self.orderedKeys { 78 | if strings.Index(envKey, findStr) >= 0 { 79 | return true 80 | } 81 | } 82 | return false 83 | } 84 | 85 | type Arg2Env struct { 86 | orderedKeys []string 87 | keyNames map[string]string 88 | nameKeys map[string]string 89 | } 90 | 91 | func newArg2Env() *Arg2Env { 92 | return &Arg2Env{nil, map[string]string{}, map[string]string{}} 93 | } 94 | 95 | func (self *Arg2Env) Add(envKey string, argName string) { 96 | old, ok := self.keyNames[envKey] 97 | if ok { 98 | panic(fmt.Errorf("[Arg2Env.Add] multi args map to env '%s', old '%s', new '%s'", 99 | envKey, old, argName)) 100 | } 101 | old, ok = self.nameKeys[argName] 102 | if ok { 103 | panic(fmt.Errorf("[Arg2Env.Add] duplicated arg name: '%s' (map to env: old '%s', new '%s')", 104 | argName, old, envKey)) 105 | } 106 | self.orderedKeys = append(self.orderedKeys, envKey) 107 | self.keyNames[envKey] = argName 108 | self.nameKeys[argName] = envKey 109 | } 110 | 111 | func (self *Arg2Env) IsEmpty() bool { 112 | return len(self.orderedKeys) == 0 113 | } 114 | 115 | func (self *Arg2Env) EnvKeys() []string { 116 | return self.orderedKeys 117 | } 118 | 119 | // TODO: put this in use 120 | func (self *Arg2Env) RenderedEnvKeys( 121 | argv ArgVals, 122 | env *Env, 123 | cmd *Cmd, 124 | allowError bool) (renderedKeys []string, origins []string, fullyRendered bool) { 125 | 126 | fullyRendered = true 127 | for _, name := range self.orderedKeys { 128 | keys, keyFullyRendered := renderTemplateStr(name, "map arg to env", cmd, argv, env, allowError) 129 | for _, key := range keys { 130 | renderedKeys = append(renderedKeys, key) 131 | origins = append(origins, name) 132 | } 133 | fullyRendered = fullyRendered && keyFullyRendered 134 | } 135 | return 136 | } 137 | 138 | func (self *Arg2Env) GetArgName(cmd *Cmd, envKey string, allowError bool) string { 139 | argName, ok := self.keyNames[envKey] 140 | if !ok { 141 | return "" 142 | } 143 | args := cmd.Args() 144 | if !args.Has(argName) { 145 | msg := fmt.Sprintf("module '%s' error: no arg %s for key %s", cmd.Owner().DisplayPath(), argName, envKey) 146 | if allowError { 147 | return "(" + msg + ")" 148 | } else { 149 | panic(fmt.Errorf("[Arg2Env.GetArgName] %s", msg)) 150 | } 151 | } 152 | return argName 153 | } 154 | 155 | func (self *Arg2Env) GetEnvKey(argName string) (string, bool) { 156 | envKey, ok := self.nameKeys[argName] 157 | return envKey, ok 158 | } 159 | 160 | func (self *Arg2Env) MatchFind(findStr string) bool { 161 | if strings.Index("write", findStr) >= 0 { 162 | return true 163 | } 164 | for argName, envKey := range self.nameKeys { 165 | if strings.Index(argName, findStr) >= 0 { 166 | return true 167 | } 168 | if strings.Index(envKey, findStr) >= 0 { 169 | return true 170 | } 171 | } 172 | return false 173 | } 174 | 175 | func (self *Arg2Env) Has(envKey string) bool { 176 | _, ok := self.keyNames[envKey] 177 | return ok 178 | } 179 | -------------------------------------------------------------------------------- /pkg/core/model/env_val.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | "time" 7 | 8 | "github.com/innerr/ticat/pkg/utils" 9 | ) 10 | 11 | func (self Env) GetRaw(name string) string { 12 | return self.Get(name).Raw 13 | } 14 | 15 | func (self Env) SetInt(name string, val int) { 16 | self.Set(name, fmt.Sprintf("%d", val)) 17 | } 18 | 19 | func (self Env) SetUint64(name string, val uint64) { 20 | self.Set(name, fmt.Sprintf("%v", val)) 21 | } 22 | 23 | func (self Env) GetInt(name string) int { 24 | val := self.Get(name).Raw 25 | if len(val) == 0 { 26 | return 0 27 | } 28 | intVal, err := strconv.Atoi(val) 29 | if err != nil { 30 | panic(&EnvValErrWrongType{ 31 | fmt.Sprintf("[EnvVal.GetInt] key '%s' = '%s' is not int: %v", name, val, err), 32 | name, val, "int", err, 33 | }) 34 | } 35 | return int(intVal) 36 | } 37 | 38 | func (self Env) GetUint64(name string) uint64 { 39 | val := self.Get(name).Raw 40 | if len(val) == 0 { 41 | return 0 42 | } 43 | intVal, err := strconv.ParseUint(val, 10, 64) 44 | if err != nil { 45 | panic(&EnvValErrWrongType{ 46 | fmt.Sprintf("[EnvVal.GetUint64] key '%s' = '%s' is not uint64: %v", name, val, err), 47 | name, val, "uint64", err, 48 | }) 49 | } 50 | return uint64(intVal) 51 | } 52 | 53 | func (self Env) GetDur(name string) time.Duration { 54 | _, ok := self.GetEx(name) 55 | if !ok { 56 | panic(&EnvValErrNotFound{ 57 | fmt.Sprintf("[EnvVal.GetDur] key '%s' not found in env", name), 58 | name, 59 | }) 60 | } 61 | val := utils.NormalizeDurStr(self.Get(name).Raw) 62 | dur, err := time.ParseDuration(val) 63 | if err != nil { 64 | panic(&EnvValErrWrongType{ 65 | fmt.Sprintf("[EnvVal.GetDur] key '%s' = '%s' is not duration format: %v", name, val, err), 66 | name, val, "Golang: time.Duration", err, 67 | }) 68 | } 69 | return dur 70 | } 71 | 72 | func (self Env) SetDur(name string, val string) { 73 | val = utils.NormalizeDurStr(val) 74 | _, err := time.ParseDuration(val) 75 | if err != nil { 76 | panic(&EnvValErrWrongType{ 77 | fmt.Sprintf("[EnvVal.SetDur] key '%s' = '%s' is not duration format: %v", name, val, err), 78 | name, val, "Golang: time.Duration", err, 79 | }) 80 | } 81 | self.Set(name, val) 82 | } 83 | 84 | func (self Env) PlusInt(name string, val int) { 85 | self.SetInt(name, self.GetInt(name)+val) 86 | } 87 | 88 | func (self Env) SetBool(name string, val bool) bool { 89 | old := StrToBool(self.Get(name).Raw) 90 | self.Set(name, fmt.Sprintf("%v", val)) 91 | return old 92 | } 93 | 94 | func (self Env) GetBool(name string) bool { 95 | return StrToBool(self.Get(name).Raw) 96 | } 97 | 98 | type EnvVal struct { 99 | Raw string 100 | IsArg bool 101 | IsSysArg bool 102 | } 103 | 104 | type EnvValErrWrongType struct { 105 | Str string 106 | Key string 107 | Val string 108 | ExpectType string 109 | ConvertErr error 110 | } 111 | 112 | func (self EnvValErrWrongType) Error() string { 113 | return self.Str 114 | } 115 | 116 | type EnvValErrNotFound struct { 117 | Err string 118 | Key string 119 | } 120 | 121 | func (self EnvValErrNotFound) Error() string { 122 | return self.Err 123 | } 124 | -------------------------------------------------------------------------------- /pkg/core/model/errs.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | type CmdError struct { 8 | Cmd ParsedCmd 9 | Err error 10 | } 11 | 12 | func WrapCmdError(cmd ParsedCmd, err error) *CmdError { 13 | return &CmdError{cmd, err} 14 | } 15 | 16 | func NewCmdError(cmd ParsedCmd, err string) *CmdError { 17 | return &CmdError{cmd, fmt.Errorf(err)} 18 | } 19 | 20 | func (self CmdError) Error() string { 21 | return self.Err.Error() 22 | } 23 | 24 | type TolerableErr struct { 25 | Err interface{} 26 | File string 27 | Source string 28 | Reason string 29 | } 30 | 31 | type ConflictedWithSameSource map[string][]TolerableErr 32 | 33 | type TolerableErrs struct { 34 | Uncatalogeds []TolerableErr 35 | ConflictedWithBuiltin ConflictedWithSameSource 36 | Conflicteds map[string]ConflictedWithSameSource 37 | } 38 | 39 | func NewTolerableErrs() *TolerableErrs { 40 | return &TolerableErrs{ 41 | nil, 42 | ConflictedWithSameSource{}, 43 | map[string]ConflictedWithSameSource{}, 44 | } 45 | } 46 | 47 | func (self *TolerableErrs) OnErr(err interface{}, source string, file string, reason string) { 48 | conflicted, ok := err.(ErrConflicted) 49 | if !ok { 50 | self.Uncatalogeds = append(self.Uncatalogeds, TolerableErr{err, file, source, reason}) 51 | return 52 | } 53 | 54 | old := conflicted.GetOldSource() 55 | var conflictedMap ConflictedWithSameSource 56 | if len(old) == 0 { 57 | conflictedMap = self.ConflictedWithBuiltin 58 | } else { 59 | conflictedMap, ok = self.Conflicteds[old] 60 | if conflictedMap == nil { 61 | conflictedMap = ConflictedWithSameSource{} 62 | self.Conflicteds[old] = conflictedMap 63 | } 64 | } 65 | 66 | list := conflictedMap[source] 67 | list = append(list, TolerableErr{err, file, source, reason}) 68 | conflictedMap[source] = list 69 | } 70 | 71 | type ErrConflicted interface { 72 | Error() string 73 | GetOldSource() string 74 | GetConflictedCmdPath() []string 75 | } 76 | 77 | type CmdTreeErrExecutableConflicted struct { 78 | Str string 79 | CmdPath []string 80 | OldSource string 81 | } 82 | 83 | func (self CmdTreeErrExecutableConflicted) Error() string { 84 | return self.Str 85 | } 86 | func (self CmdTreeErrExecutableConflicted) GetOldSource() string { 87 | return self.OldSource 88 | } 89 | func (self CmdTreeErrExecutableConflicted) GetConflictedCmdPath() []string { 90 | return self.CmdPath 91 | } 92 | 93 | type CmdTreeErrSubCmdConflicted struct { 94 | Str string 95 | ParentCmdPath []string 96 | SubCmdName string 97 | OldSource string 98 | } 99 | 100 | func (self CmdTreeErrSubCmdConflicted) Error() string { 101 | return self.Str 102 | } 103 | func (self CmdTreeErrSubCmdConflicted) GetOldSource() string { 104 | return self.OldSource 105 | } 106 | func (self CmdTreeErrSubCmdConflicted) GetConflictedCmdPath() []string { 107 | return append(self.ParentCmdPath, self.SubCmdName) 108 | } 109 | 110 | type CmdTreeErrSubAbbrConflicted struct { 111 | Str string 112 | ParentCmdPath []string 113 | Abbr string 114 | ForOldCmdName string 115 | ForNewCmdName string 116 | OldSource string 117 | } 118 | 119 | func (self CmdTreeErrSubAbbrConflicted) Error() string { 120 | return self.Str 121 | } 122 | func (self CmdTreeErrSubAbbrConflicted) GetOldSource() string { 123 | return self.OldSource 124 | } 125 | func (self CmdTreeErrSubAbbrConflicted) GetConflictedCmdPath() []string { 126 | return append(self.ParentCmdPath, self.Abbr) 127 | } 128 | 129 | type CmdMissedEnvValWhenRenderFlow struct { 130 | Str string 131 | CmdPath string 132 | MetaFilePath string 133 | Source string 134 | MissedKey string 135 | RenderingLine string 136 | Cmd *Cmd 137 | MappingArg string 138 | ArgIdx int 139 | } 140 | 141 | func (self CmdMissedEnvValWhenRenderFlow) Error() string { 142 | return self.Str 143 | } 144 | 145 | type CmdMissedArgValWhenRenderFlow struct { 146 | Str string 147 | CmdPath string 148 | MetaFilePath string 149 | Source string 150 | RenderingLine string 151 | Cmd *Cmd 152 | MissedArg string 153 | ArgIdx int 154 | } 155 | 156 | func (self CmdMissedArgValWhenRenderFlow) Error() string { 157 | return self.Str 158 | } 159 | 160 | type RunCmdFileFailed struct { 161 | Err string 162 | Cmd ParsedCmd 163 | Argv ArgVals 164 | Bin string 165 | SessionPath string 166 | LogFilePath string 167 | } 168 | 169 | func (self RunCmdFileFailed) Error() string { 170 | return self.Err 171 | } 172 | 173 | type AbortByUserErr struct { 174 | } 175 | 176 | func (self AbortByUserErr) Error() string { 177 | return "aborted by user" 178 | } 179 | 180 | func NewAbortByUserErr() *AbortByUserErr { 181 | return &AbortByUserErr{} 182 | } 183 | -------------------------------------------------------------------------------- /pkg/core/model/flow.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "io" 7 | "sort" 8 | "strings" 9 | 10 | "github.com/innerr/ticat/pkg/utils" 11 | ) 12 | 13 | func SaveFlowToStr(flow *ParsedCmds, cmdPathSep string, trivialMark string, env *Env) string { 14 | w := bytes.NewBuffer(nil) 15 | SaveFlow(w, flow, cmdPathSep, trivialMark, env) 16 | return w.String() 17 | } 18 | 19 | func SaveFlow(w io.Writer, flow *ParsedCmds, cmdPathSep string, trivialMark string, env *Env) { 20 | envPathSep := env.GetRaw("strs.env-path-sep") 21 | bracketLeft := env.GetRaw("strs.env-bracket-left") 22 | bracketRight := env.GetRaw("strs.env-bracket-right") 23 | envKeyValSep := env.GetRaw("strs.env-kv-sep") 24 | seqSep := env.GetRaw("strs.seq-sep") 25 | if len(envPathSep) == 0 || len(bracketLeft) == 0 || len(bracketRight) == 0 || 26 | len(envKeyValSep) == 0 || len(seqSep) == 0 { 27 | panic(fmt.Errorf("some predefined strs not found")) 28 | } 29 | 30 | for i, cmd := range flow.Cmds { 31 | if len(flow.Cmds) > 1 { 32 | if i == 0 { 33 | if flow.GlobalCmdIdx < 0 { 34 | fmt.Fprint(w, seqSep+" ") 35 | } 36 | } else { 37 | fmt.Fprint(w, " "+seqSep+" ") 38 | } 39 | } 40 | 41 | if cmd.ParseResult.Error != nil { 42 | fmt.Fprint(w, strings.Join(cmd.ParseResult.Input, " ")) 43 | continue 44 | } 45 | 46 | var path []string 47 | var prevSegHasNoCmd bool 48 | var cmdHasEnv bool 49 | 50 | for i := 0; i < cmd.TrivialLvl; i++ { 51 | fmt.Fprint(w, trivialMark) 52 | } 53 | 54 | for j, seg := range cmd.Segments { 55 | if len(cmd.Segments) > 1 && j != 0 && !prevSegHasNoCmd { 56 | fmt.Fprint(w, cmdPathSep) 57 | } 58 | fmt.Fprint(w, seg.Matched.Name) 59 | 60 | if seg.Matched.Cmd != nil { 61 | path = append(path, seg.Matched.Cmd.Name()) 62 | } else if len(seg.Matched.Name) != 0 { 63 | path = append(path, seg.Matched.Name) 64 | } 65 | 66 | prevSegHasNoCmd = (seg.Matched.Cmd == nil) 67 | 68 | savedEnv := false 69 | useArgsFmt := (j == len(cmd.Segments)-1) 70 | savedEnv = SaveFlowEnv(w, seg.Env, path, envPathSep, seqSep, 71 | bracketLeft, bracketRight, envKeyValSep, useArgsFmt) 72 | cmdHasEnv = cmdHasEnv || savedEnv 73 | } 74 | } 75 | } 76 | 77 | func SaveFlowEnv( 78 | w io.Writer, 79 | env ParsedEnv, 80 | prefixPath []string, 81 | pathSep string, 82 | seqSep string, 83 | bracketLeft string, 84 | bracketRight string, 85 | envKeyValSep string, 86 | useArgsFmt bool) bool { 87 | 88 | if len(env) == 0 { 89 | return false 90 | } 91 | 92 | isAllArgs := true 93 | var keys []string 94 | for k, v := range env { 95 | if !v.IsArg { 96 | isAllArgs = false 97 | } 98 | keys = append(keys, k) 99 | } 100 | sort.Strings(keys) 101 | 102 | prefix := strings.Join(prefixPath, pathSep) + pathSep 103 | 104 | var kvs []string 105 | for _, k := range keys { 106 | v := env[k] 107 | if strings.HasPrefix(k, prefix) && len(k) != len(prefix) { 108 | k = strings.Join(v.MatchedPath[len(prefixPath):], pathSep) 109 | } 110 | val := normalizeEnvVal(v.Val, seqSep) 111 | kv := fmt.Sprintf("%v%s%v", k, envKeyValSep, utils.QuoteStrIfHasSpace(val)) 112 | kvs = append(kvs, kv) 113 | } 114 | 115 | format := bracketLeft + "%s" + bracketRight 116 | if isAllArgs && useArgsFmt { 117 | format = " %s" 118 | } 119 | fmt.Fprintf(w, format, strings.Join(kvs, " ")) 120 | return true 121 | } 122 | 123 | func normalizeEnvVal(v string, seqSep string) string { 124 | i := 0 125 | for i < len(v) { 126 | origin := i 127 | i = strings.Index(v[i:], seqSep) 128 | if i < 0 { 129 | break 130 | } 131 | i += origin 132 | if i > len(BackSlash) && v[i-len(BackSlash):i] == BackSlash { 133 | i += len(seqSep) 134 | continue 135 | } 136 | // NOTE: shellwords in 'cmd.go#FlowStrToStrs' will eat the single '\', so double '\\' is needed 137 | v = v[0:i] + BackSlash + BackSlash + seqSep + v[i+len(seqSep):] 138 | i += len(BackSlash)*2 + len(seqSep) 139 | } 140 | return v 141 | } 142 | 143 | // TODO: put '\' to env 144 | // TODO: too many related logic: here, parser, shellwords 145 | const BackSlash = "\\" 146 | -------------------------------------------------------------------------------- /pkg/core/model/forest.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | type ForestMode struct { 8 | stack []string 9 | } 10 | 11 | func NewForestMode() *ForestMode { 12 | return &ForestMode{[]string{}} 13 | } 14 | 15 | func (self ForestMode) AtForestTopLvl(env *Env) bool { 16 | return len(self.stack) != 0 && self.stack[len(self.stack)-1] == GetLastStackFrame(env) 17 | } 18 | 19 | func (self *ForestMode) Pop(env *Env) { 20 | if len(self.stack) == 0 { 21 | panic(fmt.Errorf("[Forest] should never happen: pop on empty")) 22 | } 23 | last1 := GetLastStackFrame(env) 24 | last2 := self.stack[len(self.stack)-1] 25 | if last1 != last2 { 26 | panic(fmt.Errorf("[Forest] should never happen: pop on wrong frame: %s != %s", last1, last2)) 27 | } 28 | self.stack = self.stack[0 : len(self.stack)-1] 29 | } 30 | 31 | func (self *ForestMode) Push(frame string) { 32 | self.stack = append(self.stack, frame) 33 | } 34 | 35 | func (self *ForestMode) Clone() *ForestMode { 36 | stack := []string{} 37 | for _, it := range self.stack { 38 | stack = append(stack, it) 39 | } 40 | return &ForestMode{stack} 41 | } 42 | -------------------------------------------------------------------------------- /pkg/core/model/helps.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "bufio" 5 | "fmt" 6 | "os" 7 | ) 8 | 9 | type Help struct { 10 | Title string 11 | Text []string 12 | } 13 | 14 | type Helps struct { 15 | Sections []Help 16 | } 17 | 18 | func NewHelps() *Helps { 19 | return &Helps{nil} 20 | } 21 | 22 | func (self *Helps) RegHelpFile(path string) { 23 | file, err := os.Open(path) 24 | if err != nil { 25 | panic(fmt.Errorf("read help file failed: %v", err)) 26 | } 27 | defer file.Close() 28 | 29 | scanner := bufio.NewScanner(file) 30 | scanner.Split(bufio.ScanLines) 31 | var lines []string 32 | for scanner.Scan() { 33 | lines = append(lines, scanner.Text()) 34 | } 35 | 36 | self.RegHelp(lines[0], lines[1:]) 37 | } 38 | 39 | func (self *Helps) RegHelp(title string, text []string) { 40 | self.Sections = append(self.Sections, Help{title, text}) 41 | } 42 | -------------------------------------------------------------------------------- /pkg/core/model/proto.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "bufio" 5 | "fmt" 6 | "io" 7 | "os" 8 | "path/filepath" 9 | "sort" 10 | "strings" 11 | ) 12 | 13 | func EnvOutput(env *Env, writer io.Writer, sep string, filtered []string, skipDefault bool) error { 14 | defEnv := env.GetLayer(EnvLayerDefault) 15 | 16 | flatten := env.Flatten(true, filtered, false) 17 | var keys []string 18 | for k, v := range flatten { 19 | if skipDefault && defEnv.GetRaw(k) == v { 20 | continue 21 | } 22 | keys = append(keys, k) 23 | } 24 | 25 | sort.Strings(keys) 26 | for _, k := range keys { 27 | v := env.GetRaw(k) 28 | _, err := fmt.Fprintf(writer, "%s%s%s\n", k, sep, v) 29 | if err != nil { 30 | return err 31 | } 32 | } 33 | return nil 34 | } 35 | 36 | func EnvInput(env *Env, reader io.Reader, sep string, delMark string) error { 37 | scanner := bufio.NewScanner(reader) 38 | scanner.Split(bufio.ScanLines) 39 | for scanner.Scan() { 40 | text := scanner.Text() 41 | if strings.HasSuffix(text, "\n") { 42 | text = text[0 : len(text)-1] 43 | } 44 | i := strings.Index(text, sep) 45 | if i < 0 { 46 | return fmt.Errorf("[EnvInput] bad format line '%s', sep '%s'", 47 | text, sep) 48 | } 49 | key := text[0:i] 50 | val := text[i+1:] 51 | if val == delMark { 52 | env.Delete(key) 53 | } else { 54 | env.Set(key, val) 55 | } 56 | } 57 | 58 | return nil 59 | } 60 | 61 | func saveEnvToFile(env *Env, path string, sep string, filtered []string, skipDefault bool) { 62 | tmp := path + ".tmp" 63 | file, err := os.OpenFile(tmp, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644) 64 | if err != nil { 65 | panic(fmt.Errorf("[SaveEnvToFile] open env file '%s' failed: %v", tmp, err)) 66 | } 67 | defer file.Close() 68 | 69 | err = EnvOutput(env, file, sep, filtered, skipDefault) 70 | if err != nil { 71 | panic(fmt.Errorf("[SaveEnvToFile] write env file '%s' failed: %v", tmp, err)) 72 | } 73 | file.Close() 74 | 75 | err = os.Rename(tmp, path) 76 | if err != nil { 77 | panic(fmt.Errorf("[SaveEnvToFile] rename env file '%s' to '%s' failed: %v", 78 | tmp, path, err)) 79 | } 80 | } 81 | 82 | func SaveEnvToFile(env *Env, path string, sep string, skipDefault bool) { 83 | // TODO: move to default config 84 | filtered := []string{ 85 | "session", 86 | "strs.", 87 | "display.height", 88 | "display.width.max", 89 | "display.executor.displayed", 90 | "sys.stack", 91 | "sys.stack-depth", 92 | "sys.session.", 93 | "sys.interact", 94 | "sys.breakpoint", 95 | "sys.execute-wait-sec", 96 | "sys.event.", 97 | "sys.paths.", 98 | } 99 | saveEnvToFile(env, path, sep, filtered, skipDefault) 100 | } 101 | 102 | func LoadEnvFromFile(env *Env, path string, sep string, delMark string) { 103 | file, err := os.Open(path) 104 | if err != nil { 105 | if os.IsNotExist(err) { 106 | return 107 | } 108 | panic(fmt.Errorf("[LoadEnvFromFile] open local env file '%s' failed: %v", 109 | path, err)) 110 | } 111 | defer file.Close() 112 | 113 | err = EnvInput(env, file, sep, delMark) 114 | if err != nil { 115 | panic(fmt.Errorf("[LoadEnvFromFile] read local env file '%s' failed: %v", 116 | path, err)) 117 | } 118 | } 119 | 120 | func saveEnvToSessionFile(cc *Cli, env *Env, parsedCmd ParsedCmd, skipDefault bool) (sessionDir string, sessionPath string) { 121 | sep := cc.Cmds.Strs.EnvKeyValSep 122 | 123 | sessionDir = env.GetRaw("session") 124 | if len(sessionDir) == 0 { 125 | panic(NewCmdError(parsedCmd, "[Cmd.executeFile] session dir not found in env")) 126 | } 127 | sessionFileName := env.GetRaw("strs.session-env-file") 128 | if len(sessionFileName) == 0 { 129 | panic(NewCmdError(parsedCmd, "[Cmd.executeFile] session env file name not found in env")) 130 | } 131 | sessionPath = filepath.Join(sessionDir, sessionFileName) 132 | 133 | filtered := []string{ 134 | //"display.height", 135 | //"sys.stack", 136 | //"sys.stack-depth", 137 | //"sys.session.", 138 | //"sys.interact", 139 | } 140 | saveEnvToFile(env.GetLayer(EnvLayerSession), sessionPath, sep, filtered, skipDefault) 141 | return 142 | } 143 | -------------------------------------------------------------------------------- /pkg/core/model/screen.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "io" 5 | "sync" 6 | ) 7 | 8 | type QuietScreen struct { 9 | outN int 10 | } 11 | 12 | func (self *QuietScreen) Print(text string) { 13 | self.outN += 1 14 | } 15 | 16 | func (self *QuietScreen) Error(text string) { 17 | } 18 | 19 | func (self *QuietScreen) OutputtedLines() int { 20 | return self.outN 21 | } 22 | 23 | type StdScreen struct { 24 | stdout io.Writer 25 | stderr io.Writer 26 | outN int 27 | } 28 | 29 | func NewStdScreen(stdout io.Writer, stderr io.Writer) *StdScreen { 30 | return &StdScreen{ 31 | stdout: stdout, 32 | stderr: stderr, 33 | } 34 | } 35 | 36 | func (self *StdScreen) Print(text string) { 37 | self.outN += 1 38 | if self.stdout == nil { 39 | return 40 | } 41 | _, err := io.WriteString(self.stdout, text) 42 | if err != nil { 43 | panic(err) 44 | } 45 | } 46 | 47 | func (self *StdScreen) Error(text string) { 48 | if self.stderr == nil { 49 | return 50 | } 51 | _, err := io.WriteString(self.stderr, text) 52 | if err != nil { 53 | panic(err) 54 | } 55 | } 56 | 57 | func (self *StdScreen) OutputtedLines() int { 58 | return self.outN 59 | } 60 | 61 | type BgTaskScreen struct { 62 | basic *StdScreen 63 | stdout *BgStdout 64 | lock sync.Mutex 65 | } 66 | 67 | func NewBgTaskScreen() *BgTaskScreen { 68 | stdout := NewBgStdout() 69 | return &BgTaskScreen{ 70 | basic: NewStdScreen(stdout, stdout), 71 | stdout: stdout, 72 | } 73 | } 74 | 75 | func (self *BgTaskScreen) Print(text string) { 76 | self.lock.Lock() 77 | defer self.lock.Unlock() 78 | self.basic.Print(text) 79 | } 80 | 81 | func (self *BgTaskScreen) Error(text string) { 82 | self.lock.Lock() 83 | defer self.lock.Unlock() 84 | self.basic.Error(text) 85 | } 86 | 87 | func (self *BgTaskScreen) OutputtedLines() int { 88 | self.lock.Lock() 89 | defer self.lock.Unlock() 90 | return self.basic.OutputtedLines() 91 | } 92 | 93 | func (self *BgTaskScreen) GetBgStdout() *BgStdout { 94 | self.lock.Lock() 95 | defer self.lock.Unlock() 96 | return self.stdout 97 | } 98 | -------------------------------------------------------------------------------- /pkg/core/model/sys_argv.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | "time" 7 | ) 8 | 9 | type SysArgVals map[string]string 10 | 11 | func SysArgRealnameAndNormalizedValue( 12 | name string, 13 | sysArgPrefix string, 14 | value string) (realname string, normalizedValue string) { 15 | 16 | if len(name) < len(sysArgPrefix) || name[0:len(sysArgPrefix)] != sysArgPrefix { 17 | return 18 | } 19 | raw := name[len(sysArgPrefix):] 20 | if raw == SysArgNameDelay { 21 | _, err := strconv.ParseFloat(value, 64) 22 | if err == nil { 23 | value += "s" 24 | } 25 | return name, value 26 | } else if raw == SysArgNameDelayEnvApplyPolicy { 27 | if value != SysArgValueDelayEnvApplyPolicyApply { 28 | panic(fmt.Errorf("[Args.SysArgRealname] %s: the value of sys arg '%s' could only be '%s'", 29 | name, SysArgNameDelayEnvApplyPolicy, SysArgValueDelayEnvApplyPolicyApply)) 30 | } 31 | return name, value 32 | } else if raw == SysArgNameError { 33 | if value != SysArgValueOK { 34 | panic(fmt.Errorf("[Args.SysArgRealname] %s: the value of sys arg '%s' could only be '%s'", 35 | name, SysArgNameError, SysArgValueOK)) 36 | } 37 | return name, value 38 | } else { 39 | panic(fmt.Errorf("[Args.SysArgRealname] %s: only sys args could have '%s' prefix, '%s' is not sys arg", 40 | name, sysArgPrefix, raw)) 41 | } 42 | return 43 | } 44 | 45 | func (self SysArgVals) GetDelayStr() string { 46 | return self[SysArgNameDelay] 47 | } 48 | 49 | func (self SysArgVals) IsDelay() bool { 50 | return len(self[SysArgNameDelay]) != 0 51 | } 52 | 53 | func (self SysArgVals) GetDelayDuration() time.Duration { 54 | delayDur := self[SysArgNameDelay] 55 | dur, err := time.ParseDuration(delayDur) 56 | if err != nil { 57 | panic(&ArgValErrWrongType{ 58 | fmt.Sprintf("[Cmd.AsyncExecute] sys arg '%s = %s' is valid not golang duration format", SysArgNameDelay, delayDur), 59 | SysArgNameDelay, delayDur, "golan duration format", err, 60 | }) 61 | } 62 | return dur 63 | } 64 | 65 | func (self SysArgVals) IsDelayEnvEarlyApply() bool { 66 | return self[SysArgNameDelayEnvApplyPolicy] == SysArgValueDelayEnvApplyPolicyApply 67 | } 68 | 69 | func (self SysArgVals) AllowError() bool { 70 | return self[SysArgNameError] == SysArgValueOK 71 | } 72 | 73 | // TODO: put sys arg names into env strs 74 | const ( 75 | SysArgNameDelay string = "delay" 76 | SysArgNameDelayEnvApplyPolicy string = "env" 77 | SysArgNameError string = "err" 78 | 79 | SysArgValueDelayEnvApplyPolicyApply string = "apply" 80 | SysArgValueOK string = "ok" 81 | ) 82 | -------------------------------------------------------------------------------- /pkg/core/model/val.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "strings" 5 | ) 6 | 7 | func StrToBool(s string) bool { 8 | return StrToTrue(s) 9 | } 10 | 11 | func StrToTrue(s string) bool { 12 | s = strings.ToLower(s) 13 | return s == "true" || s == "t" || s == "1" || s == "on" || s == "y" || s == "yes" 14 | } 15 | 16 | func StrToFalse(s string) bool { 17 | s = strings.ToLower(s) 18 | return s == "false" || s == "f" || s == "0" || s == "off" || s == "n" || s == "no" 19 | } 20 | -------------------------------------------------------------------------------- /pkg/core/parser/cmds.go: -------------------------------------------------------------------------------- 1 | package parser 2 | 3 | import ( 4 | "github.com/innerr/ticat/pkg/core/model" 5 | ) 6 | 7 | type Parser struct { 8 | seqParser *SequenceParser 9 | cmdParser *CmdParser 10 | } 11 | 12 | // UPDATE: rewite this with goyacc 13 | // 14 | // A very simple implement of command line parsing, lack of char escaping 15 | // - The command line argv list have extra tokenizing info 16 | // - An example: a quoted string with space inside 17 | // - TODO: how to store this info(to flow file?) and still keep it human-editable ? 18 | // - The dynamite info(registered modules and env KVs) could use for disambiguation 19 | // - Inconvenient to use a LEX/YACC lib to parse 20 | func (self *Parser) Parse( 21 | cmds *model.CmdTree, 22 | envAbbrs *model.EnvAbbrs, 23 | input ...string) *model.ParsedCmds { 24 | 25 | seqs, firstIsGlobal := self.seqParser.Parse(input) 26 | flow := model.ParsedCmds{model.ParsedEnv{}, nil, -1, false, false, false} 27 | for _, seq := range seqs { 28 | flow.Cmds = append(flow.Cmds, self.cmdParser.Parse(cmds, envAbbrs, seq)) 29 | } 30 | if firstIsGlobal && len(flow.Cmds) != 0 { 31 | flow.GlobalCmdIdx = 0 32 | if !flow.Cmds[0].IsEmpty() { 33 | firstCmd := flow.Cmds[0] 34 | for _, seg := range firstCmd.Segments { 35 | flow.GlobalEnv.Merge(seg.Env) 36 | seg.Env = nil 37 | } 38 | } 39 | } 40 | return &flow 41 | } 42 | 43 | func NewParser(seqParser *SequenceParser, cmdParser *CmdParser) *Parser { 44 | return &Parser{seqParser, cmdParser} 45 | } 46 | -------------------------------------------------------------------------------- /pkg/core/parser/sequence.go: -------------------------------------------------------------------------------- 1 | package parser 2 | 3 | import ( 4 | "strings" 5 | ) 6 | 7 | type SequenceParser struct { 8 | sep string 9 | unbreakPrefixs []string 10 | unbreakSuffixs []string 11 | } 12 | 13 | func NewSequenceParser( 14 | sep string, 15 | unbreakPrefixs []string, 16 | unbreakSuffixs []string) *SequenceParser { 17 | 18 | return &SequenceParser{ 19 | sep, 20 | unbreakPrefixs, 21 | unbreakSuffixs, 22 | } 23 | } 24 | 25 | func (self *SequenceParser) Normalize(argv []string) []string { 26 | sepN := len(self.sep) 27 | res := []string{} 28 | for _, arg := range argv { 29 | if len(arg) == 0 { 30 | res = append(res, arg) 31 | } 32 | handledIdx := 0 33 | searchIdx := 0 34 | for ; handledIdx < len(arg); searchIdx += sepN { 35 | i := strings.Index(arg[searchIdx:], self.sep) 36 | if i < 0 { 37 | res = append(res, arg[handledIdx:]) 38 | break 39 | } 40 | searchIdx += i 41 | // TODO: 42 | // 1. put '\' to SequenceParser's init args 43 | // 2. this is a workaround, we also need to handle '\\', '\\\', ... things like that 44 | if searchIdx >= 1 && "\\" == arg[searchIdx-1:searchIdx] { 45 | arg = arg[0:searchIdx-1] + arg[searchIdx:] 46 | continue 47 | } 48 | unbreakable := false 49 | for _, prefix := range self.unbreakPrefixs { 50 | if searchIdx >= len(prefix) && 51 | prefix == arg[searchIdx-len(prefix):searchIdx] { 52 | unbreakable = true 53 | break 54 | } 55 | } 56 | if unbreakable { 57 | continue 58 | } 59 | for _, suffix := range self.unbreakSuffixs { 60 | if searchIdx+sepN+len(suffix) <= len(arg) && 61 | suffix == arg[searchIdx+sepN:searchIdx+sepN+len(suffix)] { 62 | unbreakable = true 63 | break 64 | } 65 | } 66 | if unbreakable { 67 | continue 68 | } 69 | if handledIdx != searchIdx { 70 | res = append(res, arg[handledIdx:searchIdx]) 71 | } 72 | res = append(res, self.sep) 73 | handledIdx = searchIdx + sepN 74 | } 75 | } 76 | return res 77 | } 78 | 79 | func (self *SequenceParser) Parse(argv []string) (parsed [][]string, firstIsGlobal bool) { 80 | argv = self.Normalize(argv) 81 | 82 | firstIsGlobal = true 83 | if len(argv) != 0 && argv[0] == self.sep { 84 | firstIsGlobal = false 85 | } 86 | 87 | parsed = [][]string{} 88 | curr := []string{} 89 | for _, arg := range argv { 90 | if arg != self.sep { 91 | arg = strings.TrimSpace(arg) 92 | //if len(arg) != 0 { 93 | curr = append(curr, arg) 94 | //} 95 | //} else if len(curr) != 0 { 96 | } else { 97 | parsed = append(parsed, curr) 98 | curr = []string{} 99 | } 100 | } 101 | //if len(curr) != 0 { 102 | parsed = append(parsed, curr) 103 | //} 104 | return 105 | } 106 | -------------------------------------------------------------------------------- /pkg/main/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/innerr/ticat/pkg/ticat" 7 | ) 8 | 9 | func main() { 10 | ticat := ticat.NewTiCat() 11 | ticat.RunCli(os.Args[1:]...) 12 | } 13 | -------------------------------------------------------------------------------- /pkg/mods/builtin/api.go: -------------------------------------------------------------------------------- 1 | package builtin 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "path/filepath" 7 | 8 | "github.com/innerr/ticat/pkg/core/model" 9 | ) 10 | 11 | func ApiCmdType( 12 | argv model.ArgVals, 13 | cc *model.Cli, 14 | env *model.Env, 15 | flow *model.ParsedCmds, 16 | currCmdIdx int) (int, bool) { 17 | 18 | assertNotTailMode(flow, currCmdIdx) 19 | cmdStr := getAndCheckArg(argv, flow.Cmds[currCmdIdx], "cmd") 20 | cmd, _ := cc.ParseCmd(true, cmdStr) 21 | node := cmd.LastCmdNode() 22 | if node != nil { 23 | if node.Cmd() != nil { 24 | fmt.Fprintf(os.Stdout, "%s\n", node.Cmd().Type()) 25 | } 26 | } 27 | return currCmdIdx, true 28 | } 29 | 30 | func ApiCmdMeta( 31 | argv model.ArgVals, 32 | cc *model.Cli, 33 | env *model.Env, 34 | flow *model.ParsedCmds, 35 | currCmdIdx int) (int, bool) { 36 | 37 | assertNotTailMode(flow, currCmdIdx) 38 | cmdStr := getAndCheckArg(argv, flow.Cmds[currCmdIdx], "cmd") 39 | cmd, _ := cc.ParseCmd(true, cmdStr) 40 | node := cmd.LastCmdNode() 41 | if node != nil { 42 | if node.Cmd() != nil { 43 | fmt.Fprintf(os.Stdout, "%s\n", node.Cmd().MetaFile()) 44 | } 45 | } 46 | return currCmdIdx, true 47 | } 48 | 49 | func ApiCmdPath( 50 | argv model.ArgVals, 51 | cc *model.Cli, 52 | env *model.Env, 53 | flow *model.ParsedCmds, 54 | currCmdIdx int) (int, bool) { 55 | 56 | assertNotTailMode(flow, currCmdIdx) 57 | cmdStr := getAndCheckArg(argv, flow.Cmds[currCmdIdx], "cmd") 58 | cmd, _ := cc.ParseCmd(true, cmdStr) 59 | node := cmd.LastCmdNode() 60 | if node != nil { 61 | cic := node.Cmd() 62 | if cic != nil { 63 | line := cic.CmdLine() 64 | if len(line) != 0 && cic.Type() != model.CmdTypeEmptyDir { 65 | fmt.Fprintf(os.Stdout, "%s\n", line) 66 | } 67 | } 68 | } 69 | return currCmdIdx, true 70 | } 71 | 72 | func ApiCmdDir( 73 | argv model.ArgVals, 74 | cc *model.Cli, 75 | env *model.Env, 76 | flow *model.ParsedCmds, 77 | currCmdIdx int) (int, bool) { 78 | 79 | assertNotTailMode(flow, currCmdIdx) 80 | cmdStr := getAndCheckArg(argv, flow.Cmds[currCmdIdx], "cmd") 81 | cmd, _ := cc.ParseCmd(true, cmdStr) 82 | node := cmd.LastCmdNode() 83 | if node != nil { 84 | cic := node.Cmd() 85 | if cic != nil { 86 | if cic.Type() == model.CmdTypeEmptyDir { 87 | fmt.Fprintf(os.Stdout, "%s\n", node.Cmd().CmdLine()) 88 | } else { 89 | dir := filepath.Dir(node.Cmd().MetaFile()) 90 | fmt.Fprintf(os.Stdout, "%s\n", dir) 91 | } 92 | } 93 | } 94 | return currCmdIdx, true 95 | } 96 | 97 | func ApiCmdListAll( 98 | argv model.ArgVals, 99 | cc *model.Cli, 100 | env *model.Env, 101 | flow *model.ParsedCmds, 102 | currCmdIdx int) (int, bool) { 103 | 104 | assertNotTailMode(flow, currCmdIdx) 105 | cmdDumpName(cc.Cmds, cc.Screen) 106 | return currCmdIdx, true 107 | } 108 | 109 | func cmdDumpName(cmd *model.CmdTree, screen model.Screen) { 110 | if !cmd.IsEmpty() { 111 | screen.Print(cmd.DisplayPath() + "\n") 112 | } 113 | for _, name := range cmd.SubNames() { 114 | cmdDumpName(cmd.GetSub(name), screen) 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /pkg/mods/builtin/bg_tasks.go: -------------------------------------------------------------------------------- 1 | package builtin 2 | 3 | import ( 4 | "github.com/innerr/ticat/pkg/cli/display" 5 | "github.com/innerr/ticat/pkg/core/model" 6 | "github.com/innerr/ticat/pkg/utils" 7 | ) 8 | 9 | func WaitForLatestBgTaskFinish( 10 | argv model.ArgVals, 11 | cc *model.Cli, 12 | env *model.Env, 13 | flow *model.ParsedCmds, 14 | currCmdIdx int) (int, bool) { 15 | 16 | assertNotTailMode(flow, currCmdIdx) 17 | 18 | if utils.GoRoutineIdStr() != utils.GoRoutineIdStrMain { 19 | panic(model.NewCmdError(flow.Cmds[currCmdIdx], 20 | "must be in main thread to wait for other threads to finish")) 21 | } 22 | 23 | tid, task, ok := cc.BgTasks.GetLatestTask() 24 | if ok { 25 | errs := WaitBgTask(cc, env, tid, task) 26 | for _, err := range errs { 27 | display.PrintError(cc, env, err) 28 | } 29 | } 30 | return currCmdIdx, true 31 | } 32 | 33 | func WaitForBgTaskFinishByName( 34 | argv model.ArgVals, 35 | cc *model.Cli, 36 | env *model.Env, 37 | flow *model.ParsedCmds, 38 | currCmdIdx int) (int, bool) { 39 | 40 | assertNotTailMode(flow, currCmdIdx) 41 | 42 | bgCmd := getAndCheckArg(argv, flow.Cmds[currCmdIdx], "command") 43 | 44 | if utils.GoRoutineIdStr() != utils.GoRoutineIdStrMain { 45 | panic(model.NewCmdError(flow.Cmds[currCmdIdx], 46 | "must be in main thread to wait for other threads to finish")) 47 | } 48 | 49 | verifiedCmd := cc.NormalizeCmd(true, bgCmd) 50 | tid, task, ok := cc.BgTasks.GetTaskByCmd(verifiedCmd) 51 | if !ok { 52 | display.PrintTipTitle(cc.Screen, env, "command '"+bgCmd+"' not in background") 53 | } else { 54 | errs := WaitBgTask(cc, env, tid, task) 55 | for _, err := range errs { 56 | display.PrintError(cc, env, err) 57 | } 58 | } 59 | return currCmdIdx, true 60 | } 61 | 62 | func WaitForAllBgTasksFinish( 63 | argv model.ArgVals, 64 | cc *model.Cli, 65 | env *model.Env, 66 | flow *model.ParsedCmds, 67 | currCmdIdx int) (int, bool) { 68 | 69 | assertNotTailMode(flow, currCmdIdx) 70 | 71 | if utils.GoRoutineIdStr() != utils.GoRoutineIdStrMain { 72 | panic(model.NewCmdError(flow.Cmds[currCmdIdx], 73 | "must be in main thread to wait for other threads to finish")) 74 | } 75 | errs := WaitBgTasks(cc, env, true) 76 | for _, err := range errs { 77 | display.PrintError(cc, env, err) 78 | } 79 | return currCmdIdx, true 80 | } 81 | 82 | func WaitBgTask(cc *model.Cli, env *model.Env, tid string, task *model.BgTask) (errs []error) { 83 | info := task.GetStat() 84 | display.PrintSwitchingThreadDisplay(utils.GoRoutineIdStr(), info, env, cc.Screen, true) 85 | 86 | cc.BgTasks.BringBgTaskToFront(tid, cc.CmdIO.CmdStdout) 87 | err := task.WaitForFinish() 88 | if err != nil { 89 | errs = append(errs, err) 90 | } 91 | cc.BgTasks.RemoveTask(tid) 92 | return 93 | } 94 | 95 | func WaitBgTasks(cc *model.Cli, env *model.Env, manual bool) (errs []error) { 96 | preTid := utils.GoRoutineIdStr() 97 | for { 98 | tid, task, ok := cc.BgTasks.GetEarliestTask() 99 | if !ok { 100 | break 101 | } 102 | info := task.GetStat() 103 | 104 | display.PrintSwitchingThreadDisplay(preTid, info, env, cc.Screen, manual) 105 | 106 | cc.BgTasks.BringBgTaskToFront(tid, cc.CmdIO.CmdStdout) 107 | err := task.WaitForFinish() 108 | if err != nil { 109 | errs = append(errs, err) 110 | } 111 | cc.BgTasks.RemoveTask(tid) 112 | preTid = tid 113 | } 114 | return 115 | } 116 | -------------------------------------------------------------------------------- /pkg/mods/builtin/blender.go: -------------------------------------------------------------------------------- 1 | package builtin 2 | 3 | import ( 4 | "github.com/innerr/ticat/pkg/core/model" 5 | ) 6 | 7 | func SetForestMode( 8 | argv model.ArgVals, 9 | cc *model.Cli, 10 | env *model.Env, 11 | flow *model.ParsedCmds, 12 | currCmdIdx int) (int, bool) { 13 | 14 | assertNotTailMode(flow, currCmdIdx) 15 | 16 | if !cc.ForestMode.AtForestTopLvl(env) { 17 | cc.ForestMode.Push(model.GetLastStackFrame(env)) 18 | } 19 | return currCmdIdx, true 20 | } 21 | 22 | func BlenderReplaceOnce( 23 | argv model.ArgVals, 24 | cc *model.Cli, 25 | env *model.Env, 26 | flow *model.ParsedCmds, 27 | currCmdIdx int) (int, bool) { 28 | 29 | assertNotTailMode(flow, currCmdIdx) 30 | 31 | src := getAndCheckArg(argv, flow.Cmds[currCmdIdx], "src") 32 | dest := getAndCheckArg(argv, flow.Cmds[currCmdIdx], "dest") 33 | 34 | srcCmd, _ := cc.ParseCmd(true, src) 35 | destCmd, _ := cc.ParseCmd(true, model.FlowStrToStrs(dest)...) 36 | cc.Blender.AddReplace(srcCmd, destCmd, 1) 37 | return currCmdIdx, true 38 | } 39 | 40 | func BlenderReplaceForAll( 41 | argv model.ArgVals, 42 | cc *model.Cli, 43 | env *model.Env, 44 | flow *model.ParsedCmds, 45 | currCmdIdx int) (int, bool) { 46 | 47 | assertNotTailMode(flow, currCmdIdx) 48 | 49 | src := getAndCheckArg(argv, flow.Cmds[currCmdIdx], "src") 50 | dest := getAndCheckArg(argv, flow.Cmds[currCmdIdx], "dest") 51 | 52 | srcCmd, _ := cc.ParseCmd(true, src) 53 | destCmd, _ := cc.ParseCmd(true, model.FlowStrToStrs(dest)...) 54 | cc.Blender.AddReplace(srcCmd, destCmd, -1) 55 | return currCmdIdx, true 56 | } 57 | 58 | func BlenderRemoveOnce( 59 | argv model.ArgVals, 60 | cc *model.Cli, 61 | env *model.Env, 62 | flow *model.ParsedCmds, 63 | currCmdIdx int) (int, bool) { 64 | 65 | assertNotTailMode(flow, currCmdIdx) 66 | targetStr := getAndCheckArg(argv, flow.Cmds[currCmdIdx], "target") 67 | target, _ := cc.ParseCmd(true, targetStr) 68 | cc.Blender.AddRemove(target, 1) 69 | return currCmdIdx, true 70 | } 71 | 72 | func BlenderRemoveForAll( 73 | argv model.ArgVals, 74 | cc *model.Cli, 75 | env *model.Env, 76 | flow *model.ParsedCmds, 77 | currCmdIdx int) (int, bool) { 78 | 79 | assertNotTailMode(flow, currCmdIdx) 80 | targetStr := getAndCheckArg(argv, flow.Cmds[currCmdIdx], "target") 81 | target, _ := cc.ParseCmd(true, targetStr) 82 | cc.Blender.AddRemove(target, -1) 83 | return currCmdIdx, true 84 | } 85 | 86 | func BlenderInsertOnce( 87 | argv model.ArgVals, 88 | cc *model.Cli, 89 | env *model.Env, 90 | flow *model.ParsedCmds, 91 | currCmdIdx int) (int, bool) { 92 | 93 | assertNotTailMode(flow, currCmdIdx) 94 | targetStr := getAndCheckArg(argv, flow.Cmds[currCmdIdx], "target") 95 | newStr := getAndCheckArg(argv, flow.Cmds[currCmdIdx], "new") 96 | 97 | target, _ := cc.ParseCmd(true, targetStr) 98 | newCmd, _ := cc.ParseCmd(true, model.FlowStrToStrs(newStr)...) 99 | cc.Blender.AddInsert(target, newCmd, 1) 100 | return currCmdIdx, true 101 | } 102 | 103 | func BlenderInsertForAll( 104 | argv model.ArgVals, 105 | cc *model.Cli, 106 | env *model.Env, 107 | flow *model.ParsedCmds, 108 | currCmdIdx int) (int, bool) { 109 | 110 | assertNotTailMode(flow, currCmdIdx) 111 | targetStr := getAndCheckArg(argv, flow.Cmds[currCmdIdx], "target") 112 | newStr := getAndCheckArg(argv, flow.Cmds[currCmdIdx], "new") 113 | 114 | target, _ := cc.ParseCmd(true, targetStr) 115 | newCmd, _ := cc.ParseCmd(true, model.FlowStrToStrs(newStr)...) 116 | cc.Blender.AddInsert(target, newCmd, -1) 117 | return currCmdIdx, true 118 | } 119 | 120 | func BlenderInsertAfterOnce( 121 | argv model.ArgVals, 122 | cc *model.Cli, 123 | env *model.Env, 124 | flow *model.ParsedCmds, 125 | currCmdIdx int) (int, bool) { 126 | 127 | assertNotTailMode(flow, currCmdIdx) 128 | targetStr := getAndCheckArg(argv, flow.Cmds[currCmdIdx], "target") 129 | newStr := getAndCheckArg(argv, flow.Cmds[currCmdIdx], "new") 130 | 131 | target, _ := cc.ParseCmd(true, targetStr) 132 | newCmd, _ := cc.ParseCmd(true, model.FlowStrToStrs(newStr)...) 133 | cc.Blender.AddInsertAfter(target, newCmd, 1) 134 | return currCmdIdx, true 135 | } 136 | 137 | func BlenderInsertAfterForAll( 138 | argv model.ArgVals, 139 | cc *model.Cli, 140 | env *model.Env, 141 | flow *model.ParsedCmds, 142 | currCmdIdx int) (int, bool) { 143 | 144 | assertNotTailMode(flow, currCmdIdx) 145 | targetStr := getAndCheckArg(argv, flow.Cmds[currCmdIdx], "target") 146 | newStr := getAndCheckArg(argv, flow.Cmds[currCmdIdx], "new") 147 | 148 | target, _ := cc.ParseCmd(true, targetStr) 149 | newCmd, _ := cc.ParseCmd(true, model.FlowStrToStrs(newStr)...) 150 | cc.Blender.AddInsertAfter(target, newCmd, -1) 151 | return currCmdIdx, true 152 | } 153 | 154 | func BlenderClear( 155 | argv model.ArgVals, 156 | cc *model.Cli, 157 | env *model.Env, 158 | flow *model.ParsedCmds, 159 | currCmdIdx int) (int, bool) { 160 | 161 | assertNotTailMode(flow, currCmdIdx) 162 | cc.Blender.Clear() 163 | return currCmdIdx, true 164 | } 165 | -------------------------------------------------------------------------------- /pkg/mods/builtin/display.go: -------------------------------------------------------------------------------- 1 | package builtin 2 | 3 | import ( 4 | "runtime" 5 | 6 | "github.com/innerr/ticat/pkg/core/model" 7 | "github.com/innerr/ticat/pkg/utils" 8 | ) 9 | 10 | func LoadPlatformDisplay( 11 | argv model.ArgVals, 12 | cc *model.Cli, 13 | env *model.Env, 14 | flow *model.ParsedCmds, 15 | currCmdIdx int) (int, bool) { 16 | 17 | assertNotTailMode(flow, currCmdIdx) 18 | 19 | env = env.GetLayer(model.EnvLayerDefault) 20 | switch runtime.GOOS { 21 | case "linux": 22 | env.Set("display.utf8.symbols.tip", " ☻ ") 23 | } 24 | return currCmdIdx, true 25 | } 26 | 27 | func SetDisplayStyle( 28 | argv model.ArgVals, 29 | cc *model.Cli, 30 | env *model.Env, 31 | flow *model.ParsedCmds, 32 | currCmdIdx int) (int, bool) { 33 | 34 | assertNotTailMode(flow, currCmdIdx) 35 | 36 | style := argv.GetRaw("style") 37 | env = env.GetLayer(model.EnvLayerSession) 38 | env.Set("display.style", style) 39 | return currCmdIdx, true 40 | } 41 | 42 | func SetDisplayWidth( 43 | argv model.ArgVals, 44 | cc *model.Cli, 45 | env *model.Env, 46 | flow *model.ParsedCmds, 47 | currCmdIdx int) (int, bool) { 48 | 49 | assertNotTailMode(flow, currCmdIdx) 50 | 51 | width := argv.GetInt("width") 52 | if width == 0 { 53 | _, width = utils.GetTerminalWidth(50, 100) 54 | } 55 | env = env.GetLayer(model.EnvLayerSession) 56 | env.SetInt("display.width", width) 57 | return currCmdIdx, true 58 | } 59 | -------------------------------------------------------------------------------- /pkg/mods/builtin/dump_cmd.go: -------------------------------------------------------------------------------- 1 | package builtin 2 | 3 | import ( 4 | "github.com/innerr/ticat/pkg/cli/display" 5 | "github.com/innerr/ticat/pkg/core/model" 6 | ) 7 | 8 | func DumpCmdUsage( 9 | argv model.ArgVals, 10 | cc *model.Cli, 11 | env *model.Env, 12 | flow *model.ParsedCmds, 13 | currCmdIdx int) (int, bool) { 14 | 15 | cmdPath := tailModeCallArg(flow, currCmdIdx, argv, "cmd-path") 16 | dumpArgs := display.NewDumpCmdArgs().SetSkeleton().SetShowUsage().NoRecursive() 17 | dumpCmdByPath(cc, env, dumpArgs, cmdPath, "cmd.full") 18 | return currCmdIdx, true 19 | } 20 | 21 | func DumpCmdWithDetails( 22 | argv model.ArgVals, 23 | cc *model.Cli, 24 | env *model.Env, 25 | flow *model.ParsedCmds, 26 | currCmdIdx int) (int, bool) { 27 | 28 | cmdPath := tailModeCallArg(flow, currCmdIdx, argv, "cmd-path") 29 | dumpArgs := display.NewDumpCmdArgs().NoRecursive() 30 | dumpCmdByPath(cc, env, dumpArgs, cmdPath, "") 31 | return currCmdIdx, true 32 | } 33 | 34 | func DumpTailCmdWithUsage( 35 | argv model.ArgVals, 36 | cc *model.Cli, 37 | env *model.Env, 38 | flow *model.ParsedCmds, 39 | currCmdIdx int) (int, bool) { 40 | 41 | err := flow.FirstErr() 42 | if err != nil { 43 | panic(err.Error) 44 | } 45 | 46 | cmdPath := argv.GetRaw("cmd-path") 47 | if len(cmdPath) == 0 { 48 | cmdPath = flow.Last().DisplayPath(cc.Cmds.Strs.PathSep, false) 49 | } else { 50 | cmdPath = cc.NormalizeCmd(true, cmdPath) 51 | } 52 | dumpArgs := display.NewDumpCmdArgs().SetSkeleton().SetShowUsage().NoRecursive() 53 | dumpCmdByPath(cc, env, dumpArgs, cmdPath, "==") 54 | return clearFlow(flow) 55 | } 56 | 57 | func DumpTailCmdWithDetails( 58 | argv model.ArgVals, 59 | cc *model.Cli, 60 | env *model.Env, 61 | flow *model.ParsedCmds, 62 | currCmdIdx int) (int, bool) { 63 | 64 | err := flow.FirstErr() 65 | if err != nil { 66 | panic(err.Error) 67 | } 68 | 69 | cmdPath := argv.GetRaw("cmd-path") 70 | if len(cmdPath) == 0 { 71 | cmdPath = flow.Last().DisplayPath(cc.Cmds.Strs.PathSep, false) 72 | } else { 73 | cmdPath = cc.NormalizeCmd(true, cmdPath) 74 | } 75 | dumpArgs := display.NewDumpCmdArgs().NoRecursive() 76 | dumpCmdByPath(cc, env, dumpArgs, cmdPath, "") 77 | return clearFlow(flow) 78 | } 79 | 80 | func dumpCmdByPath(cc *model.Cli, env *model.Env, args *display.DumpCmdArgs, path string, fullDetailCmd string) { 81 | cmds := cc.Cmds 82 | if len(path) != 0 { 83 | cmds = cmds.GetSubByPath(path, true) 84 | } 85 | if args.Skeleton { 86 | if len(fullDetailCmd) != 0 { 87 | display.PrintTipTitle(cc.Screen, env, 88 | "command usage: (use '"+fullDetailCmd+"' for full details)") 89 | } else { 90 | display.PrintTipTitle(cc.Screen, env, "command usage:") 91 | } 92 | } else { 93 | display.PrintTipTitle(cc.Screen, env, "full command details:") 94 | } 95 | display.DumpCmds(cmds, cc.Screen, env, args) 96 | } 97 | -------------------------------------------------------------------------------- /pkg/mods/builtin/dump_cmds.go: -------------------------------------------------------------------------------- 1 | package builtin 2 | 3 | import ( 4 | "github.com/innerr/ticat/pkg/cli/display" 5 | "github.com/innerr/ticat/pkg/core/model" 6 | ) 7 | 8 | func DumpCmds( 9 | argv model.ArgVals, 10 | cc *model.Cli, 11 | env *model.Env, 12 | flow *model.ParsedCmds, 13 | currCmdIdx int) (int, bool) { 14 | 15 | dumpArgs := display.NewDumpCmdArgs().SetSkeleton() 16 | return dumpCmds(argv, cc, env, flow, currCmdIdx, dumpArgs, "", "cmds.more") 17 | } 18 | 19 | func DumpCmdsWithUsage( 20 | argv model.ArgVals, 21 | cc *model.Cli, 22 | env *model.Env, 23 | flow *model.ParsedCmds, 24 | currCmdIdx int) (int, bool) { 25 | 26 | dumpArgs := display.NewDumpCmdArgs().SetSkeleton().SetShowUsage() 27 | return dumpCmds(argv, cc, env, flow, currCmdIdx, dumpArgs, "cmds", "cmds.full") 28 | } 29 | 30 | func DumpCmdsWithDetails( 31 | argv model.ArgVals, 32 | cc *model.Cli, 33 | env *model.Env, 34 | flow *model.ParsedCmds, 35 | currCmdIdx int) (int, bool) { 36 | 37 | dumpArgs := display.NewDumpCmdArgs() 38 | return dumpCmds(argv, cc, env, flow, currCmdIdx, dumpArgs, "cmds.more", "") 39 | } 40 | 41 | func DumpTailCmdSub( 42 | argv model.ArgVals, 43 | cc *model.Cli, 44 | env *model.Env, 45 | flow *model.ParsedCmds, 46 | currCmdIdx int) (int, bool) { 47 | 48 | dumpArgs := display.NewDumpCmdArgs().SetSkeleton() 49 | return dumpTailCmdSub(argv, cc, env, flow, currCmdIdx, dumpArgs, "", "tail-sub.more") 50 | } 51 | 52 | func DumpTailCmdSubWithUsage( 53 | argv model.ArgVals, 54 | cc *model.Cli, 55 | env *model.Env, 56 | flow *model.ParsedCmds, 57 | currCmdIdx int) (int, bool) { 58 | 59 | dumpArgs := display.NewDumpCmdArgs().SetSkeleton().SetShowUsage() 60 | return dumpTailCmdSub(argv, cc, env, flow, currCmdIdx, dumpArgs, "tail-sub", "tail-sub.full") 61 | } 62 | 63 | func DumpTailCmdSubWithDetails( 64 | argv model.ArgVals, 65 | cc *model.Cli, 66 | env *model.Env, 67 | flow *model.ParsedCmds, 68 | currCmdIdx int) (int, bool) { 69 | 70 | dumpArgs := display.NewDumpCmdArgs() 71 | return dumpTailCmdSub(argv, cc, env, flow, currCmdIdx, dumpArgs, "tail-sub.more", "") 72 | } 73 | 74 | func dumpCmds( 75 | argv model.ArgVals, 76 | cc *model.Cli, 77 | env *model.Env, 78 | flow *model.ParsedCmds, 79 | currCmdIdx int, 80 | dumpArgs *display.DumpCmdArgs, 81 | lessDetailCmd string, 82 | moreDetailCmd string) (int, bool) { 83 | 84 | findStrs := getFindStrsFromArgvAndFlow(flow, currCmdIdx, argv) 85 | if len(findStrs) != 0 { 86 | dumpArgs.AddFindStrs(findStrs...) 87 | } 88 | 89 | cmdPath := argv.GetRaw("cmd-path") 90 | cmds := cc.Cmds 91 | if len(cmdPath) != 0 { 92 | cmds = cmds.GetSubByPath(cmdPath, true) 93 | } 94 | 95 | tag := argv.GetRaw("tag") 96 | if len(tag) != 0 { 97 | if len(findStrs) != 0 { 98 | panic(model.NewCmdError(flow.Cmds[currCmdIdx], "not support mix-search with tag and keywords now")) 99 | } 100 | dumpArgs.AddFindStrs(tag).SetFindByTags() 101 | } 102 | 103 | source := argv.GetRaw("source") 104 | if len(source) != 0 { 105 | dumpArgs.SetSource(source) 106 | } 107 | 108 | if len(argv.GetRaw("depth")) != 0 { 109 | dumpArgs.SetMaxDepth(argv.GetInt("depth")) 110 | } 111 | 112 | display.DumpCmdsWithTips(cmds, cc.Screen, env, dumpArgs, cmdPath, lessDetailCmd, moreDetailCmd) 113 | return clearFlow(flow) 114 | } 115 | 116 | func dumpTailCmdSub( 117 | argv model.ArgVals, 118 | cc *model.Cli, 119 | env *model.Env, 120 | flow *model.ParsedCmds, 121 | currCmdIdx int, 122 | dumpArgs *display.DumpCmdArgs, 123 | lessDetailCmd string, 124 | moreDetailCmd string) (int, bool) { 125 | 126 | err := flow.FirstErr() 127 | if err != nil { 128 | panic(err.Error) 129 | } 130 | 131 | findStrs := getFindStrsFromArgv(argv) 132 | if len(findStrs) != 0 { 133 | dumpArgs.AddFindStrs(findStrs...) 134 | } 135 | 136 | source := argv.GetRaw("source") 137 | if len(source) != 0 { 138 | dumpArgs.SetSource(source) 139 | } 140 | 141 | if len(argv.GetRaw("depth")) != 0 { 142 | dumpArgs.SetMaxDepth(argv.GetInt("depth")) 143 | } 144 | 145 | cmdPath := flow.Last().DisplayPath(cc.Cmds.Strs.PathSep, false) 146 | cmds := cc.Cmds 147 | if len(cmdPath) != 0 { 148 | cmds = cmds.GetSubByPath(cmdPath, true) 149 | } 150 | 151 | display.DumpCmdsWithTips(cmds, cc.Screen, env, dumpArgs, cmdPath, lessDetailCmd, moreDetailCmd) 152 | return clearFlow(flow) 153 | } 154 | -------------------------------------------------------------------------------- /pkg/mods/builtin/dump_env.go: -------------------------------------------------------------------------------- 1 | package builtin 2 | 3 | import ( 4 | "github.com/innerr/ticat/pkg/cli/display" 5 | "github.com/innerr/ticat/pkg/core/model" 6 | ) 7 | 8 | // TODO: this can't be colorize, because it share codes with executor display(with frame, color will break len(str)) 9 | func DumpEnvTree( 10 | argv model.ArgVals, 11 | cc *model.Cli, 12 | env *model.Env, 13 | flow *model.ParsedCmds, 14 | currCmdIdx int) (int, bool) { 15 | 16 | assertNotTailMode(flow, currCmdIdx) 17 | display.PrintTipTitle(cc.Screen, env, "all env key-values:") 18 | display.DumpEnvTree(cc.Screen, env, 4) 19 | return currCmdIdx, true 20 | } 21 | 22 | func DumpEnvAbbrs( 23 | argv model.ArgVals, 24 | cc *model.Cli, 25 | env *model.Env, 26 | flow *model.ParsedCmds, 27 | currCmdIdx int) (int, bool) { 28 | 29 | assertNotTailMode(flow, currCmdIdx) 30 | display.PrintTipTitle(cc.Screen, env, "all env key abbrs:") 31 | display.DumpEnvAbbrs(cc, env, 4) 32 | return currCmdIdx, true 33 | } 34 | 35 | func DumpEnvFlattenVals( 36 | argv model.ArgVals, 37 | cc *model.Cli, 38 | env *model.Env, 39 | flow *model.ParsedCmds, 40 | currCmdIdx int) (int, bool) { 41 | 42 | findStrs := getFindStrsFromArgvAndFlow(flow, currCmdIdx, argv) 43 | 44 | screen := display.NewCacheScreen() 45 | display.DumpEnvFlattenVals(screen, env, findStrs...) 46 | if screen.OutputtedLines() <= 0 { 47 | display.PrintTipTitle(cc.Screen, env, "no matched env keys.") 48 | } else if len(findStrs) == 0 { 49 | display.PrintTipTitle(cc.Screen, env, "all env key-values:") 50 | } else { 51 | display.PrintTipTitle(cc.Screen, env, "all matched env key-values:") 52 | } 53 | screen.WriteTo(cc.Screen) 54 | if display.TooMuchOutput(env, screen) { 55 | display.PrintTipTitle(cc.Screen, env, 56 | "filter env keys by:", 57 | "", 58 | display.SuggestFindEnv(env, ".ls")) 59 | } 60 | return currCmdIdx, true 61 | } 62 | 63 | func DumpEssentialEnvFlattenVals( 64 | argv model.ArgVals, 65 | cc *model.Cli, 66 | env *model.Env, 67 | flow *model.ParsedCmds, 68 | currCmdIdx int) (int, bool) { 69 | 70 | findStrs := getFindStrsFromArgvAndFlow(flow, currCmdIdx, argv) 71 | 72 | screen := display.NewCacheScreen() 73 | display.DumpEssentialEnvFlattenVals(screen, env, findStrs...) 74 | if screen.OutputtedLines() <= 0 { 75 | if len(findStrs) != 0 { 76 | display.PrintTipTitle(cc.Screen, env, 77 | "no matched changed env key-values.", 78 | "(system's are not included)", 79 | "", 80 | display.SuggestListEnv(env)) 81 | } else { 82 | display.PrintTipTitle(cc.Screen, env, 83 | "no changed env key-values found.", 84 | "(system's are not included)", 85 | "", 86 | "env usage:", 87 | "", 88 | display.SuggestAddAndSaveEnv(env)) 89 | } 90 | } else if len(findStrs) == 0 { 91 | display.PrintTipTitle(cc.Screen, env, 92 | "essential env key-values: (use command 'env.ls' to show all)") 93 | } else { 94 | display.PrintTipTitle(cc.Screen, env, 95 | "matched essential env key-values: (use command 'env.ls' to show all)") 96 | } 97 | screen.WriteTo(cc.Screen) 98 | if display.TooMuchOutput(env, screen) { 99 | display.PrintTipTitle(cc.Screen, env, 100 | "filter env keys by:", 101 | "", 102 | display.SuggestFindEnv(env, "")) 103 | } 104 | return currCmdIdx, true 105 | } 106 | -------------------------------------------------------------------------------- /pkg/mods/builtin/env_snapshot.go: -------------------------------------------------------------------------------- 1 | package builtin 2 | 3 | import ( 4 | "fmt" 5 | "io/fs" 6 | "os" 7 | "path/filepath" 8 | "strings" 9 | 10 | "github.com/innerr/ticat/pkg/cli/display" 11 | "github.com/innerr/ticat/pkg/core/model" 12 | ) 13 | 14 | func EnvSaveToSnapshot( 15 | argv model.ArgVals, 16 | cc *model.Cli, 17 | env *model.Env, 18 | flow *model.ParsedCmds, 19 | currCmdIdx int) (int, bool) { 20 | 21 | assertNotTailMode(flow, currCmdIdx) 22 | kvSep := env.GetRaw("strs.env-kv-sep") 23 | 24 | cmd := flow.Cmds[currCmdIdx] 25 | name := getAndCheckArg(argv, cmd, "snapshot-name") 26 | path := getEnvSnapshotPath(env, name) 27 | 28 | overwrite := argv.GetBool("overwrite") 29 | if !overwrite && fileExists(path) { 30 | panic(model.NewCmdError(cmd, "env snapshot '"+name+"' already exists")) 31 | } 32 | 33 | model.SaveEnvToFile(env, path, kvSep, true) 34 | display.PrintTipTitle(cc.Screen, env, 35 | "session env are saved to snapshot '"+name+"', could be use by:", 36 | "", 37 | display.SuggestLoadEnvSnapshot(env)) 38 | 39 | return currCmdIdx, true 40 | } 41 | 42 | func EnvRemoveSnapshot( 43 | argv model.ArgVals, 44 | cc *model.Cli, 45 | env *model.Env, 46 | flow *model.ParsedCmds, 47 | currCmdIdx int) (int, bool) { 48 | 49 | assertNotTailMode(flow, currCmdIdx) 50 | 51 | cmd := flow.Cmds[currCmdIdx] 52 | name := getAndCheckArg(argv, cmd, "snapshot-name") 53 | path := getEnvSnapshotPath(env, name) 54 | 55 | err := os.Remove(path) 56 | if err != nil { 57 | if os.IsNotExist(err) { 58 | display.PrintTipTitle(cc.Screen, env, 59 | fmt.Sprintf("env snapshot '%s' not exists\n", name)) 60 | } else { 61 | panic(model.NewCmdError(cmd, 62 | fmt.Sprintf("remove env snapshot file '%s' failed: %v", path, err))) 63 | } 64 | } else { 65 | display.PrintTipTitle(cc.Screen, env, 66 | "env snapshot '"+name+"' is removed") 67 | } 68 | 69 | return currCmdIdx, true 70 | } 71 | 72 | func EnvListSnapshots( 73 | argv model.ArgVals, 74 | cc *model.Cli, 75 | env *model.Env, 76 | flow *model.ParsedCmds, 77 | currCmdIdx int) (int, bool) { 78 | 79 | assertNotTailMode(flow, currCmdIdx) 80 | 81 | cmd := flow.Cmds[currCmdIdx] 82 | ext := env.GetRaw("strs.env-snapshot-ext") 83 | if len(ext) == 0 { 84 | panic(model.NewCmdError(cmd, "env value 'strs.env-snapshot-ext' is empty")) 85 | } 86 | 87 | root := getEnvSnapshotDir(env) 88 | var names []string 89 | 90 | filepath.Walk(root, func(path string, info fs.FileInfo, err error) error { 91 | if path == root { 92 | return nil 93 | } 94 | if !strings.HasSuffix(path, ext) { 95 | return nil 96 | } 97 | name := path[len(root)+1:] 98 | name = name[0 : len(name)-len(ext)] 99 | names = append(names, name) 100 | return nil 101 | }) 102 | 103 | if len(names) > 0 { 104 | var title string 105 | if len(names) == 1 { 106 | title = "has 1 saved env snapshot." 107 | } else { 108 | title = fmt.Sprintf("have %v saved env snapshots.", len(names)) 109 | } 110 | display.PrintTipTitle(cc.Screen, env, 111 | title, 112 | "", 113 | "could be use by:", 114 | "", 115 | display.SuggestLoadEnvSnapshot(env)) 116 | for _, name := range names { 117 | fmt.Println(name) 118 | } 119 | } 120 | 121 | return currCmdIdx, true 122 | } 123 | 124 | func EnvLoadFromSnapshot( 125 | argv model.ArgVals, 126 | cc *model.Cli, 127 | env *model.Env, 128 | flow *model.ParsedCmds, 129 | currCmdIdx int) (int, bool) { 130 | 131 | assertNotTailMode(flow, currCmdIdx) 132 | 133 | cmd := flow.Cmds[currCmdIdx] 134 | name := getAndCheckArg(argv, cmd, "snapshot-name") 135 | path := getEnvSnapshotPath(env, name) 136 | 137 | sep := cc.Cmds.Strs.EnvKeyValSep 138 | delMark := cc.Cmds.Strs.EnvValDelAllMark 139 | 140 | loaded := model.NewEnv() 141 | model.LoadEnvFromFile(loaded, path, sep, delMark) 142 | loaded.WriteCurrLayerTo(env.GetLayer(model.EnvLayerSession)) 143 | 144 | return currCmdIdx, true 145 | } 146 | 147 | func EnvLoadNonExistFromSnapshot( 148 | argv model.ArgVals, 149 | cc *model.Cli, 150 | env *model.Env, 151 | flow *model.ParsedCmds, 152 | currCmdIdx int) (int, bool) { 153 | 154 | assertNotTailMode(flow, currCmdIdx) 155 | 156 | cmd := flow.Cmds[currCmdIdx] 157 | name := getAndCheckArg(argv, cmd, "snapshot-name") 158 | path := getEnvSnapshotPath(env, name) 159 | 160 | sep := cc.Cmds.Strs.EnvKeyValSep 161 | delMark := cc.Cmds.Strs.EnvValDelAllMark 162 | 163 | loaded := model.NewEnv() 164 | model.LoadEnvFromFile(loaded, path, sep, delMark) 165 | 166 | envSession := env.GetLayer(model.EnvLayerSession) 167 | for k, v := range loaded.FlattenAll() { 168 | if !env.Has(k) { 169 | envSession.Set(k, v) 170 | } 171 | } 172 | 173 | return currCmdIdx, true 174 | } 175 | 176 | func getEnvSnapshotDir(env *model.Env) string { 177 | dir := env.GetRaw("sys.paths.env.snapshot") 178 | if len(dir) == 0 { 179 | panic(fmt.Errorf("env value 'sys.paths.env.snapshot' is empty")) 180 | } 181 | os.MkdirAll(dir, os.ModePerm) 182 | return dir 183 | } 184 | 185 | func getEnvSnapshotPath(env *model.Env, name string) string { 186 | ext := env.GetRaw("strs.env-snapshot-ext") 187 | if len(ext) == 0 { 188 | panic(fmt.Errorf("env value 'strs.env-snapshot-ext' is empty")) 189 | } 190 | dir := getEnvSnapshotDir(env) 191 | return filepath.Join(dir, name) + ext 192 | } 193 | -------------------------------------------------------------------------------- /pkg/mods/builtin/help.go: -------------------------------------------------------------------------------- 1 | package builtin 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/innerr/ticat/pkg/cli/display" 7 | "github.com/innerr/ticat/pkg/core/model" 8 | ) 9 | 10 | func GlobalHelp( 11 | argv model.ArgVals, 12 | cc *model.Cli, 13 | env *model.Env, 14 | flow *model.ParsedCmds, 15 | currCmdIdx int) (int, bool) { 16 | 17 | assertNotTailMode(flow, currCmdIdx) 18 | 19 | target := argv.GetRaw("target") 20 | if len(target) != 0 { 21 | cmdPath := cc.NormalizeCmd(false, target) 22 | if len(cmdPath) == 0 { 23 | display.PrintErrTitle(cc.Screen, env, fmt.Sprintf("'%s' is not a valid command", target)) 24 | } else { 25 | dumpArgs := display.NewDumpCmdArgs().SetSkeleton().SetShowUsage().NoRecursive() 26 | dumpCmdByPath(cc, env, dumpArgs, cmdPath, "") 27 | } 28 | } else { 29 | display.PrintGlobalHelp(cc, env) 30 | } 31 | return currCmdIdx, true 32 | } 33 | 34 | func SelfHelp( 35 | argv model.ArgVals, 36 | cc *model.Cli, 37 | env *model.Env, 38 | flow *model.ParsedCmds, 39 | currCmdIdx int) (int, bool) { 40 | 41 | assertNotTailMode(flow, currCmdIdx) 42 | display.PrintSelfHelp(cc.Screen, env) 43 | return currCmdIdx, true 44 | } 45 | 46 | func GlobalFindCmds( 47 | argv model.ArgVals, 48 | cc *model.Cli, 49 | env *model.Env, 50 | flow *model.ParsedCmds, 51 | currCmdIdx int) (int, bool) { 52 | 53 | dumpArgs := display.NewDumpCmdArgs().SetSkeleton() 54 | return globalFindCmds(argv, cc, env, flow, currCmdIdx, dumpArgs, "", "find.more") 55 | } 56 | 57 | func GlobalFindCmdsWithUsage( 58 | argv model.ArgVals, 59 | cc *model.Cli, 60 | env *model.Env, 61 | flow *model.ParsedCmds, 62 | currCmdIdx int) (int, bool) { 63 | 64 | dumpArgs := display.NewDumpCmdArgs().SetSkeleton().SetShowUsage() 65 | return globalFindCmds(argv, cc, env, flow, currCmdIdx, dumpArgs, "find", "find.full") 66 | } 67 | 68 | func GlobalFindCmdsWithDetails( 69 | argv model.ArgVals, 70 | cc *model.Cli, 71 | env *model.Env, 72 | flow *model.ParsedCmds, 73 | currCmdIdx int) (int, bool) { 74 | 75 | dumpArgs := display.NewDumpCmdArgs() 76 | return globalFindCmds(argv, cc, env, flow, currCmdIdx, dumpArgs, "find.more", "") 77 | } 78 | 79 | func globalFindCmds( 80 | argv model.ArgVals, 81 | cc *model.Cli, 82 | env *model.Env, 83 | flow *model.ParsedCmds, 84 | currCmdIdx int, 85 | dumpArgs *display.DumpCmdArgs, 86 | lessDetailCmd string, 87 | moreDetailCmd string) (int, bool) { 88 | 89 | findStrs := getFindStrsFromArgvAndFlow(flow, currCmdIdx, argv) 90 | if len(findStrs) != 0 { 91 | dumpArgs.AddFindStrs(findStrs...) 92 | } 93 | display.DumpCmdsWithTips(cc.Cmds, cc.Screen, env, dumpArgs, "", lessDetailCmd, moreDetailCmd) 94 | return clearFlow(flow) 95 | } 96 | 97 | func DumpCmdsWhoWriteKey( 98 | argv model.ArgVals, 99 | cc *model.Cli, 100 | env *model.Env, 101 | flow *model.ParsedCmds, 102 | currCmdIdx int) (int, bool) { 103 | 104 | key := tailModeCallArg(flow, currCmdIdx, argv, "key") 105 | dumpArgs := display.NewDumpCmdArgs().SetSkeleton().SetMatchWriteKey(key) 106 | 107 | screen := display.NewCacheScreen() 108 | display.DumpCmds(cc.Cmds, screen, env, dumpArgs) 109 | 110 | if screen.OutputtedLines() > 0 { 111 | display.PrintTipTitle(cc.Screen, env, "all commands which write key '"+key+"':") 112 | } else { 113 | display.PrintTipTitle(cc.Screen, env, "no command writes key '"+key+"':") 114 | } 115 | screen.WriteTo(cc.Screen) 116 | return currCmdIdx, true 117 | } 118 | 119 | func DumpCmdsTree( 120 | argv model.ArgVals, 121 | cc *model.Cli, 122 | env *model.Env, 123 | flow *model.ParsedCmds, 124 | currCmdIdx int) (int, bool) { 125 | 126 | err := flow.FirstErr() 127 | if err != nil { 128 | panic(err.Error) 129 | } 130 | 131 | dumpArgs := display.NewDumpCmdArgs().SetSkeleton().NoFlatten() 132 | 133 | cmdPath := "" 134 | cmds := cc.Cmds 135 | if len(argv.GetRaw("cmd-path")) != 0 { 136 | cmdPath = tailModeCallArg(flow, currCmdIdx, argv, "cmd-path") 137 | cmds = cmds.GetSubByPath(cmdPath, true) 138 | } 139 | 140 | depth := 0 141 | if len(argv.GetRaw("depth")) != 0 { 142 | depth = argv.GetInt("depth") 143 | dumpArgs.SetMaxDepth(depth) 144 | } 145 | 146 | screen := display.NewCacheScreen() 147 | allShown := display.DumpCmds(cmds, screen, env, dumpArgs) 148 | 149 | text := "" 150 | if len(cmdPath) == 0 { 151 | text = "the tree of all commands:" 152 | } else { 153 | text = "the tree branch of '" + cmdPath + "'" 154 | } 155 | if !allShown { 156 | text += fmt.Sprintf(", some may not shown by arg depth='%d'", depth) 157 | } 158 | 159 | display.PrintTipTitle(cc.Screen, env, text) 160 | screen.WriteTo(cc.Screen) 161 | return clearFlow(flow) 162 | } 163 | -------------------------------------------------------------------------------- /pkg/mods/builtin/join.go: -------------------------------------------------------------------------------- 1 | package builtin 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "strconv" 7 | "strings" 8 | 9 | "github.com/innerr/ticat/pkg/core/model" 10 | ) 11 | 12 | const ( 13 | joinKvName = "join.kvs" 14 | ) 15 | 16 | type joinKv struct { 17 | Key string `json:"key"` 18 | Vals []string `json:"vals"` 19 | } 20 | 21 | func JoinNew( 22 | argv model.ArgVals, 23 | cc *model.Cli, 24 | env *model.Env, 25 | flow *model.ParsedCmds, 26 | currCmdIdx int) (int, bool) { 27 | 28 | assertNotTailMode(flow, currCmdIdx) 29 | 30 | key := argv.GetRaw("key") 31 | val := argv.GetRaw("value") 32 | 33 | if key == "" || val == "" { 34 | panic(model.NewCmdError(flow.Cmds[currCmdIdx], 35 | "need key or values")) 36 | } 37 | 38 | vals := []string{} 39 | if strings.HasPrefix(val, "[") && strings.HasSuffix(val, "]") && strings.Count(val, ",") <= 2 && strings.Count(val, ",") >= 1 { 40 | i := strings.Index(val, ",") 41 | j := strings.LastIndex(val, ",") 42 | var startStr, endStr, stepStr string 43 | if i != j { 44 | startStr = val[1:i] 45 | endStr = val[i+1 : j] 46 | stepStr = val[j+1 : len(val)-1] 47 | } else { 48 | startStr = val[1:j] 49 | endStr = val[j+1 : len(val)-1] 50 | stepStr = "1" 51 | } 52 | if strings.Contains(val, ".") { 53 | start, _ := strconv.ParseFloat(startStr, 64) 54 | end, _ := strconv.ParseFloat(endStr, 64) 55 | step, _ := strconv.ParseFloat(stepStr, 64) 56 | for ; start <= end; start += step { 57 | vals = append(vals, fmt.Sprintf("%f", start)) 58 | } 59 | } else { 60 | start, _ := strconv.Atoi(startStr) 61 | end, _ := strconv.Atoi(endStr) 62 | step, _ := strconv.Atoi(stepStr) 63 | for ; start <= end; start += step { 64 | vals = append(vals, strconv.Itoa(start)) 65 | } 66 | } 67 | } else { 68 | vals = strings.Split(val, env.GetRaw("strs.list-sep")) 69 | } 70 | 71 | kv := joinKv{ 72 | Key: key, 73 | Vals: vals, 74 | } 75 | kvsStr := env.GetRaw(joinKvName) 76 | kvs := []joinKv{} 77 | if kvsStr != "" { 78 | if err := json.Unmarshal([]byte(kvsStr), &kvs); err != nil { 79 | panic(model.NewCmdError(flow.Cmds[currCmdIdx], 80 | fmt.Sprintf("parse argument '%s' failed: %s", kvsStr, err.Error()))) 81 | } 82 | } 83 | kvs = append(kvs, kv) 84 | data, err := json.Marshal(kvs) 85 | if err != nil { 86 | panic(model.NewCmdError(flow.Cmds[currCmdIdx], 87 | fmt.Sprintf("encode argument '%s' failed: %s", kvsStr, err.Error()))) 88 | } 89 | env.GetLayer(model.EnvLayerSession).Set(joinKvName, string(data)) 90 | return currCmdIdx, true 91 | } 92 | 93 | func JoinRun( 94 | argv model.ArgVals, 95 | cc *model.Cli, 96 | env *model.Env, 97 | flow *model.ParsedCmds, 98 | currCmdIdx int) (int, bool) { 99 | 100 | assertNotTailMode(flow, currCmdIdx) 101 | 102 | kvs := []joinKv{} 103 | kvsStr := env.GetRaw(joinKvName) 104 | if err := json.Unmarshal([]byte(kvsStr), &kvs); err != nil { 105 | panic(model.NewCmdError(flow.Cmds[currCmdIdx], 106 | fmt.Sprintf("parse argument '%s' failed: %s", kvsStr, err.Error()))) 107 | } 108 | cmd := argv.GetRaw("cmd") 109 | if cmd == "" { 110 | panic(model.NewCmdError(flow.Cmds[currCmdIdx], 111 | "can't execute null ticat command")) 112 | } 113 | iter := newJoinKvsIter(kvs) 114 | for { 115 | args := []string{cmd} 116 | keys, vals := iter.get() 117 | for i, key := range keys { 118 | args = append(args, key+"="+vals[i]) 119 | } 120 | cc.Executor.Execute(flow.Cmds[currCmdIdx].DisplayPath(cc.Cmds.Strs.PathSep, false), 121 | true, cc, env, nil, args...) 122 | if !iter.next() { 123 | break 124 | } 125 | } 126 | return currCmdIdx, true 127 | } 128 | 129 | type joinKvsIter struct { 130 | kvs []joinKv 131 | ids []int 132 | } 133 | 134 | func newJoinKvsIter(kvs []joinKv) *joinKvsIter { 135 | iter := &joinKvsIter{ 136 | kvs: kvs, 137 | ids: make([]int, len(kvs)), 138 | } 139 | return iter 140 | } 141 | 142 | func (iter *joinKvsIter) next() (ok bool) { 143 | l := len(iter.ids) 144 | for i := l - 1; i >= 0; i-- { 145 | if iter.ids[i] < len(iter.kvs[i].Vals)-1 { 146 | ok = true 147 | break 148 | } 149 | } 150 | if ok { 151 | for i := l - 1; i >= 0; i-- { 152 | if iter.ids[i] < len(iter.kvs[i].Vals)-1 { 153 | iter.ids[i]++ 154 | break 155 | } else { 156 | iter.ids[i] = 0 157 | } 158 | } 159 | } 160 | return 161 | } 162 | 163 | func (iter *joinKvsIter) get() (keys, vals []string) { 164 | for i := range iter.ids { 165 | keys = append(keys, iter.kvs[i].Key) 166 | vals = append(vals, iter.kvs[i].Vals[iter.ids[i]]) 167 | } 168 | return 169 | } 170 | -------------------------------------------------------------------------------- /pkg/mods/builtin/mod.go: -------------------------------------------------------------------------------- 1 | package builtin 2 | 3 | import ( 4 | "io/fs" 5 | "os" 6 | "path/filepath" 7 | "strings" 8 | 9 | "github.com/innerr/ticat/pkg/core/model" 10 | "github.com/innerr/ticat/pkg/mods/persist/mod_meta" 11 | "github.com/innerr/ticat/pkg/utils" 12 | ) 13 | 14 | func SetExtExec( 15 | argv model.ArgVals, 16 | cc *model.Cli, 17 | env *model.Env, 18 | flow *model.ParsedCmds, 19 | currCmdIdx int) (int, bool) { 20 | 21 | assertNotTailMode(flow, currCmdIdx) 22 | env = env.GetLayer(model.EnvLayerDefault) 23 | env.Set("sys.ext.exec.bash", "bash") 24 | env.Set("sys.ext.exec.sh", "sh") 25 | env.Set("sys.ext.exec.py", utils.FindPython()) 26 | env.Set("sys.ext.exec.go", "go run") 27 | return currCmdIdx, true 28 | } 29 | 30 | func loadLocalMods( 31 | cc *model.Cli, 32 | root string, 33 | reposFileName string, 34 | metaExt string, 35 | flowExt string, 36 | helpExt string, 37 | abbrsSep string, 38 | envPathSep string, 39 | source string, 40 | panicRecover bool) { 41 | 42 | if len(root) > 0 && root[len(root)-1] == filepath.Separator { 43 | root = root[:len(root)-1] 44 | } 45 | 46 | // TODO: return filepath.SkipDir to avoid some non-sense scanning 47 | filepath.Walk(root, func(metaPath string, info fs.FileInfo, err error) error { 48 | if info != nil && info.IsDir() { 49 | // Skip hidden file or dir 50 | base := filepath.Base(metaPath) 51 | if len(base) > 0 && base[0] == '.' { 52 | return filepath.SkipDir 53 | } 54 | return nil 55 | } 56 | if metaPath == filepath.Join(root, reposFileName) { 57 | return nil 58 | } 59 | 60 | if strings.HasSuffix(metaPath, helpExt) { 61 | cc.Helps.RegHelpFile(metaPath) 62 | return nil 63 | } 64 | 65 | if strings.HasSuffix(metaPath, flowExt) { 66 | cmdPath := filepath.Base(metaPath[0 : len(metaPath)-len(flowExt)]) 67 | cmdPaths := strings.Split(cmdPath, cc.Cmds.Strs.PathSep) 68 | mod_meta.RegMod(cc, metaPath, "", false, true, cmdPaths, 69 | flowExt, abbrsSep, envPathSep, source, panicRecover) 70 | return nil 71 | } 72 | 73 | ext := filepath.Ext(metaPath) 74 | if ext != metaExt { 75 | return nil 76 | } 77 | targetPath := metaPath[0 : len(metaPath)-len(ext)] 78 | 79 | // Note: strip all ext(s) from cmd-path 80 | cmdPath := targetPath[len(root)+1:] 81 | for { 82 | ext := filepath.Ext(cmdPath) 83 | if len(ext) == 0 { 84 | break 85 | } else { 86 | cmdPath = cmdPath[0 : len(cmdPath)-len(ext)] 87 | } 88 | } 89 | 90 | isDir := false 91 | info, err = os.Stat(targetPath) 92 | if os.IsNotExist(err) { 93 | targetPath = "" 94 | } else if err == nil { 95 | isDir = info.IsDir() 96 | } 97 | 98 | cmdPaths := strings.Split(cmdPath, string(filepath.Separator)) 99 | mod_meta.RegMod(cc, metaPath, targetPath, isDir, false, cmdPaths, 100 | flowExt, abbrsSep, envPathSep, source, panicRecover) 101 | return nil 102 | }) 103 | } 104 | -------------------------------------------------------------------------------- /pkg/mods/builtin/system.go: -------------------------------------------------------------------------------- 1 | package builtin 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "os/exec" 7 | 8 | "github.com/innerr/ticat/pkg/core/model" 9 | ) 10 | 11 | func ExecCmds( 12 | argv model.ArgVals, 13 | cc *model.Cli, 14 | env *model.Env, 15 | flow *model.ParsedCmds, 16 | currCmdIdx int) (int, bool) { 17 | 18 | assertNotTailMode(flow, currCmdIdx) 19 | 20 | cmdStr := argv.GetRaw("command") 21 | if cmdStr == "" { 22 | panic(model.NewCmdError(flow.Cmds[currCmdIdx], 23 | "can't execute null os command")) 24 | } 25 | cmd := exec.Command("bash", "-c", cmdStr) 26 | cmd.Stdin = os.Stdin 27 | cmd.Stdout = os.Stdout 28 | cmd.Stderr = os.Stderr 29 | if err := cmd.Run(); err != nil { 30 | panic(model.NewCmdError(flow.Cmds[currCmdIdx], 31 | fmt.Sprintf("execute os command '%s' failed: %s", cmdStr, err.Error()))) 32 | } 33 | return currCmdIdx, true 34 | } 35 | -------------------------------------------------------------------------------- /pkg/mods/builtin/tags.go: -------------------------------------------------------------------------------- 1 | package builtin 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/innerr/ticat/pkg/cli/display" 7 | "github.com/innerr/ticat/pkg/core/model" 8 | ) 9 | 10 | func ListTags( 11 | argv model.ArgVals, 12 | cc *model.Cli, 13 | env *model.Env, 14 | flow *model.ParsedCmds, 15 | currCmdIdx int) (int, bool) { 16 | 17 | assertNotTailMode(flow, currCmdIdx) 18 | 19 | screen := display.NewCacheScreen() 20 | display.ListTags(cc.Cmds, screen, env) 21 | if screen.OutputtedLines() > 0 { 22 | display.PrintTipTitle(cc.Screen, env, "all tags:") 23 | } else { 24 | display.PrintTipTitle(cc.Screen, env, "no command have tags") 25 | } 26 | screen.WriteTo(cc.Screen) 27 | return currCmdIdx, true 28 | } 29 | 30 | func FindByTags( 31 | argv model.ArgVals, 32 | cc *model.Cli, 33 | env *model.Env, 34 | flow *model.ParsedCmds, 35 | currCmdIdx int) (int, bool) { 36 | 37 | dumpArgs := display.NewDumpCmdArgs().SetSkeleton() 38 | return findByTags(argv, cc, env, flow, currCmdIdx, dumpArgs) 39 | } 40 | 41 | func FindByTagsWithUsage( 42 | argv model.ArgVals, 43 | cc *model.Cli, 44 | env *model.Env, 45 | flow *model.ParsedCmds, 46 | currCmdIdx int) (int, bool) { 47 | 48 | dumpArgs := display.NewDumpCmdArgs().SetSkeleton().SetShowUsage() 49 | return findByTags(argv, cc, env, flow, currCmdIdx, dumpArgs) 50 | } 51 | 52 | func FindByTagsWithDetails( 53 | argv model.ArgVals, 54 | cc *model.Cli, 55 | env *model.Env, 56 | flow *model.ParsedCmds, 57 | currCmdIdx int) (int, bool) { 58 | 59 | dumpArgs := display.NewDumpCmdArgs() 60 | return findByTags(argv, cc, env, flow, currCmdIdx, dumpArgs) 61 | } 62 | 63 | func findByTags( 64 | argv model.ArgVals, 65 | cc *model.Cli, 66 | env *model.Env, 67 | flow *model.ParsedCmds, 68 | currCmdIdx int, 69 | dumpArgs *display.DumpCmdArgs) (int, bool) { 70 | 71 | findStrs := getFindStrsFromArgvAndFlow(flow, currCmdIdx, argv) 72 | if len(findStrs) == 0 { 73 | return ListTags(argv, cc, env, flow, currCmdIdx) 74 | } else { 75 | dumpArgs.AddFindStrs(findStrs...).SetFindByTags() 76 | } 77 | 78 | findStr := strings.Join(dumpArgs.FindStrs, " ") 79 | if len(findStrs) > 1 { 80 | findStr = "tags '" + findStr 81 | } else { 82 | findStr = "tag '" + findStr 83 | } 84 | findStr = " commands matched " + findStr + "'" 85 | 86 | screen := display.NewCacheScreen() 87 | display.DumpCmds(cc.Cmds, screen, env, dumpArgs) 88 | if screen.OutputtedLines() > 0 { 89 | display.PrintTipTitle(cc.Screen, env, "search and found"+findStr) 90 | } else { 91 | display.PrintTipTitle(cc.Screen, env, "search but no"+findStr+", change keywords and retry") 92 | } 93 | 94 | screen.WriteTo(cc.Screen) 95 | 96 | if display.TooMuchOutput(env, screen) { 97 | if !dumpArgs.Skeleton || dumpArgs.ShowUsage { 98 | text := append([]string{"get a brief view by:", ""}, display.SuggestFindByTagLite(env)...) 99 | display.PrintTipTitle(cc.Screen, env, text) 100 | } else if len(findStrs) > 1 { 101 | display.PrintTipTitle(cc.Screen, env, 102 | "narrow down results by using less tags") 103 | } 104 | } 105 | return currCmdIdx, true 106 | } 107 | -------------------------------------------------------------------------------- /pkg/mods/builtin/utils.go: -------------------------------------------------------------------------------- 1 | package builtin 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "os/exec" 7 | "path/filepath" 8 | "strings" 9 | 10 | "github.com/innerr/ticat/pkg/core/model" 11 | ) 12 | 13 | func assertNotTailMode(flow *model.ParsedCmds, currCmdIdx int) { 14 | if flow.HasTailMode && !flow.TailModeCall && flow.Cmds[currCmdIdx].TailMode && len(flow.Cmds) > 1 { 15 | panic(model.NewCmdError(flow.Cmds[currCmdIdx], "tail-mode not support")) 16 | } 17 | } 18 | 19 | /* 20 | func assertNotTailModeFlow(flow *model.ParsedCmds, currCmdIdx int) { 21 | if flow.HasTailMode && !flow.TailModeCall && flow.Cmds[currCmdIdx].TailMode && len(flow.Cmds) > 1 { 22 | panic(model.NewCmdError(flow.Cmds[currCmdIdx], "tail-mode flow not support")) 23 | } 24 | } 25 | 26 | func assertNotTailModeCall(flow *model.ParsedCmds, currCmdIdx int) { 27 | if flow.TailModeCall { 28 | panic(model.NewCmdError(flow.Cmds[currCmdIdx], "tail-mode call not support")) 29 | } 30 | } 31 | */ 32 | 33 | func tailModeCallArg( 34 | flow *model.ParsedCmds, 35 | currCmdIdx int, 36 | argv model.ArgVals, 37 | arg string) string { 38 | 39 | args := tailModeCallArgs(flow, currCmdIdx, argv, arg, false) 40 | return args[0] 41 | } 42 | 43 | func tailModeCallArgs( 44 | flow *model.ParsedCmds, 45 | currCmdIdx int, 46 | argv model.ArgVals, 47 | arg string, 48 | allowMultiArgs bool) []string { 49 | 50 | val := argv.GetRaw(arg) 51 | if flow.TailModeCall && !flow.Cmds[currCmdIdx].TailMode { 52 | panic(model.NewCmdError(flow.Cmds[currCmdIdx], 53 | "should not happen: wrong command tail-mode flag")) 54 | } 55 | if !flow.TailModeCall { 56 | if len(val) == 0 { 57 | panic(model.NewCmdError(flow.Cmds[currCmdIdx], "arg '"+arg+"' is empty")) 58 | } 59 | return []string{val} 60 | } 61 | 62 | args := tailModeGetInput(flow, currCmdIdx, false) 63 | flowInputN := len(args) 64 | if len(val) != 0 { 65 | args = append(args, val) 66 | } else { 67 | if len(args) == 0 { 68 | panic(model.NewCmdError(flow.Cmds[currCmdIdx], "arg '"+arg+"' is empty")) 69 | } 70 | } 71 | if !allowMultiArgs && len(args) > 1 { 72 | if flowInputN > 0 && len(val) != 0 { 73 | panic(model.NewCmdError(flow.Cmds[currCmdIdx], 74 | "mixed usage of arg '"+arg+"' and tail-mode call")) 75 | } else { 76 | panic(model.NewCmdError(flow.Cmds[currCmdIdx], 77 | "too many input of arg '"+arg+"' in tail-mode call")) 78 | } 79 | } 80 | return args 81 | } 82 | 83 | func tailModeGetInput(flow *model.ParsedCmds, currCmdIdx int, allowMultiCmds bool) (input []string) { 84 | if !flow.Cmds[currCmdIdx].TailMode { 85 | return 86 | } 87 | if len(flow.Cmds) <= 1 { 88 | return 89 | } 90 | if !allowMultiCmds { 91 | cmd := flow.Cmds[len(flow.Cmds)-1] 92 | input = append(input, cmd.ParseResult.Input...) 93 | } else { 94 | for _, cmd := range flow.Cmds[currCmdIdx+1:] { 95 | input = append(input, cmd.ParseResult.Input...) 96 | } 97 | } 98 | return 99 | } 100 | 101 | func clearFlow(flow *model.ParsedCmds) (int, bool) { 102 | flow.Cmds = nil 103 | return 0, true 104 | } 105 | 106 | func getFindStrsFromArgvAndFlow(flow *model.ParsedCmds, currCmdIdx int, argv model.ArgVals) (findStrs []string) { 107 | findStrs = getFindStrsFromArgv(argv) 108 | if flow.TailModeCall && flow.Cmds[currCmdIdx].TailMode { 109 | findStrs = append(findStrs, tailModeGetInput(flow, currCmdIdx, false)...) 110 | } 111 | return 112 | } 113 | 114 | func getFindStrsFromArgv(argv model.ArgVals) (findStrs []string) { 115 | names := []string{ 116 | "1st-str", 117 | "2nd-str", 118 | "3rd-str", 119 | "4th-str", 120 | } 121 | for _, name := range names { 122 | val := argv.GetRaw(name) 123 | if len(val) != 0 { 124 | findStrs = append(findStrs, val) 125 | } 126 | } 127 | return 128 | } 129 | 130 | func addFindStrArgs(cmd *model.Cmd) { 131 | cmd.AddArg("1st-str", "", "find-str"). 132 | AddArg("2nd-str", ""). 133 | AddArg("3rd-str", ""). 134 | AddArg("4th-str", "") 135 | } 136 | 137 | func normalizeCmdPath(path string, sep string, alterSeps string) string { 138 | var segs []string 139 | for len(path) > 0 { 140 | i := strings.IndexAny(path, alterSeps) 141 | if i < 0 { 142 | segs = append(segs, path) 143 | break 144 | } else if i == 0 { 145 | path = path[1:] 146 | } else { 147 | segs = append(segs, path[0:i]) 148 | path = path[i+1:] 149 | } 150 | } 151 | return strings.Join(segs, sep) 152 | } 153 | 154 | func getCmdPath(path string, flowExt string, cmd model.ParsedCmd) string { 155 | base := filepath.Base(path) 156 | if !strings.HasSuffix(base, flowExt) { 157 | panic(model.NewCmdError(cmd, fmt.Sprintf("flow file '%s' ext not match '%s'", path, flowExt))) 158 | } 159 | return base[:len(base)-len(flowExt)] 160 | } 161 | 162 | func getAndCheckArg(argv model.ArgVals, cmd model.ParsedCmd, arg string) string { 163 | val := argv.GetRaw(arg) 164 | if len(val) == 0 { 165 | panic(model.NewCmdError(cmd, "arg '"+arg+"' is empty")) 166 | } 167 | return val 168 | } 169 | 170 | func isOsCmdExists(cmd string) bool { 171 | path, err := exec.LookPath(cmd) 172 | return err == nil && len(path) > 0 173 | } 174 | 175 | func osRemoveDir(path string, cmd model.ParsedCmd) { 176 | path = strings.TrimSpace(path) 177 | if len(path) <= 1 { 178 | panic(model.NewCmdError(cmd, fmt.Sprintf("removing path '%v', looks not right", path))) 179 | } 180 | err := os.RemoveAll(path) 181 | if err != nil { 182 | if os.IsNotExist(err) { 183 | return 184 | } 185 | panic(model.NewCmdError(cmd, fmt.Sprintf("remove repo '%s' failed: %v", path, err))) 186 | } 187 | } 188 | 189 | func fileExists(path string) bool { 190 | info, err := os.Stat(path) 191 | return !os.IsNotExist(err) && !info.IsDir() 192 | } 193 | 194 | func dirExists(path string) bool { 195 | info, err := os.Stat(path) 196 | return !os.IsNotExist(err) && info.IsDir() 197 | } 198 | -------------------------------------------------------------------------------- /pkg/mods/builtin/verb.go: -------------------------------------------------------------------------------- 1 | package builtin 2 | 3 | import ( 4 | "github.com/innerr/ticat/pkg/core/model" 5 | ) 6 | 7 | func SetQuietMode( 8 | argv model.ArgVals, 9 | cc *model.Cli, 10 | env *model.Env, 11 | flow *model.ParsedCmds, 12 | currCmdIdx int) (int, bool) { 13 | 14 | assertNotTailMode(flow, currCmdIdx) 15 | 16 | env = env.GetLayer(model.EnvLayerSession) 17 | env.SetBool("display.executor", false) 18 | env.SetBool("display.env", false) 19 | env.SetBool("display.env.layer", false) 20 | env.SetBool("display.env.default", false) 21 | env.SetBool("display.env.display", false) 22 | env.SetBool("display.env.sys", false) 23 | env.SetBool("display.mod.quiet", false) 24 | env.SetBool("display.mod.realname", false) 25 | env.SetInt("display.max-cmd-cnt", 14) 26 | return currCmdIdx, true 27 | } 28 | 29 | func SetVerbMode( 30 | argv model.ArgVals, 31 | cc *model.Cli, 32 | env *model.Env, 33 | flow *model.ParsedCmds, 34 | currCmdIdx int) (int, bool) { 35 | 36 | assertNotTailMode(flow, currCmdIdx) 37 | 38 | env = env.GetLayer(model.EnvLayerSession) 39 | env.SetBool("display.executor", true) 40 | env.SetBool("display.env", true) 41 | env.SetBool("display.env.layer", true) 42 | env.SetBool("display.env.default", true) 43 | env.SetBool("display.env.display", true) 44 | env.SetBool("display.env.sys", true) 45 | env.SetBool("display.mod.quiet", true) 46 | env.SetBool("display.mod.realname", true) 47 | env.SetInt("display.max-cmd-cnt", 9999) 48 | env.SetBool("display.executor.end", true) 49 | return currCmdIdx, true 50 | } 51 | 52 | func IncreaseVerb( 53 | argv model.ArgVals, 54 | cc *model.Cli, 55 | env *model.Env, 56 | flow *model.ParsedCmds, 57 | currCmdIdx int) (int, bool) { 58 | 59 | assertNotTailMode(flow, currCmdIdx) 60 | 61 | env = env.GetLayer(model.EnvLayerSession) 62 | 63 | volume := argv.GetInt("volume") 64 | if volume <= 0 { 65 | return currCmdIdx, true 66 | } 67 | 68 | if !env.SetBool("display.executor", true) { 69 | volume -= 1 70 | } 71 | env.SetInt("display.max-cmd-cnt", 15) 72 | env.SetBool("display.mod.realname", true) 73 | env.SetBool("display.one-cmd", true) 74 | if volume <= 0 { 75 | return currCmdIdx, true 76 | } 77 | 78 | if !env.SetBool("display.env", true) { 79 | volume -= 1 80 | } 81 | env.SetInt("display.max-cmd-cnt", 16) 82 | if volume <= 0 { 83 | return currCmdIdx, true 84 | } 85 | 86 | if !env.SetBool("display.env.layer", true) { 87 | volume -= 1 88 | } 89 | env.SetBool("display.executor.end", true) 90 | env.SetInt("display.max-cmd-cnt", 17) 91 | if volume <= 0 { 92 | return currCmdIdx, true 93 | } 94 | 95 | if !env.SetBool("display.env.default", true) { 96 | volume -= 1 97 | } 98 | env.SetBool("display.env.display", true) 99 | env.SetInt("display.max-cmd-cnt", 18) 100 | if volume <= 0 { 101 | return currCmdIdx, true 102 | } 103 | 104 | if !env.SetBool("display.mod.quiet", true) { 105 | volume -= 1 106 | } 107 | env.SetInt("display.max-cmd-cnt", 19) 108 | if volume <= 0 { 109 | return currCmdIdx, true 110 | } 111 | 112 | if !env.SetBool("display.env.sys", true) { 113 | volume -= 1 114 | } 115 | env.SetInt("display.max-cmd-cnt", 20) 116 | if volume <= 0 { 117 | return currCmdIdx, true 118 | } 119 | 120 | env.SetInt("display.max-cmd-cnt", 9999) 121 | return currCmdIdx, true 122 | } 123 | 124 | func DecreaseVerb( 125 | argv model.ArgVals, 126 | cc *model.Cli, 127 | env *model.Env, 128 | flow *model.ParsedCmds, 129 | currCmdIdx int) (int, bool) { 130 | 131 | assertNotTailMode(flow, currCmdIdx) 132 | env = env.GetLayer(model.EnvLayerSession) 133 | 134 | volume := argv.GetInt("volume") 135 | if volume <= 0 { 136 | return currCmdIdx, true 137 | } 138 | 139 | env.SetInt("display.max-cmd-cnt", 20) 140 | 141 | if env.SetBool("display.env.sys", false) { 142 | volume -= 1 143 | } 144 | env.SetInt("display.max-cmd-cnt", 19) 145 | if volume <= 0 { 146 | return currCmdIdx, true 147 | } 148 | 149 | if env.SetBool("display.mod.quiet", false) { 150 | volume -= 1 151 | } 152 | env.SetInt("display.max-cmd-cnt", 18) 153 | if volume <= 0 { 154 | return currCmdIdx, true 155 | } 156 | 157 | if env.SetBool("display.env.default", false) { 158 | volume -= 1 159 | } 160 | env.SetBool("display.env.display", false) 161 | env.SetInt("display.max-cmd-cnt", 17) 162 | if volume <= 0 { 163 | return currCmdIdx, true 164 | } 165 | 166 | if env.SetBool("display.env.layer", false) { 167 | volume -= 1 168 | } 169 | env.SetBool("display.executor.end", false) 170 | env.SetInt("display.max-cmd-cnt", 16) 171 | if volume <= 0 { 172 | return currCmdIdx, true 173 | } 174 | 175 | if env.SetBool("display.env", false) { 176 | volume -= 1 177 | } 178 | env.SetInt("display.max-cmd-cnt", 15) 179 | if volume <= 0 { 180 | return currCmdIdx, true 181 | } 182 | 183 | if env.SetBool("display.executor", false) { 184 | volume -= 1 185 | } 186 | env.SetInt("display.max-cmd-cnt", 14) 187 | 188 | env.SetBool("display.one-cmd", false) 189 | env.SetBool("display.mod.realname", false) 190 | 191 | return currCmdIdx, true 192 | } 193 | 194 | func SetToDefaultVerb( 195 | argv model.ArgVals, 196 | cc *model.Cli, 197 | env *model.Env, 198 | flow *model.ParsedCmds, 199 | currCmdIdx int) (int, bool) { 200 | 201 | assertNotTailMode(flow, currCmdIdx) 202 | env = env.GetLayer(model.EnvLayerSession) 203 | setToDefaultVerb(env) 204 | return currCmdIdx, true 205 | } 206 | -------------------------------------------------------------------------------- /pkg/mods/persist/flow_file/flow_file.go: -------------------------------------------------------------------------------- 1 | package flow_file 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/innerr/ticat/pkg/mods/persist/meta_file" 8 | ) 9 | 10 | func LoadFlowFile(path string) (flow []string, help string, abbrs string, 11 | trivial string, autoArgs string, packSub string) { 12 | 13 | metas := meta_file.NewMetaFile(path) 14 | if len(metas) != 1 { 15 | panic(fmt.Errorf("can't load content for edit from a combined flow file")) 16 | } 17 | meta := metas[0].Meta 18 | 19 | section := meta.GetGlobalSection() 20 | help = section.Get("help") 21 | trivial = section.Get("trivial") 22 | abbrs = section.Get("abbrs") 23 | if len(abbrs) == 0 { 24 | abbrs = section.Get("abbr") 25 | } 26 | autoArgs = section.Get("args.auto") 27 | packSub = section.Get("pack-subflow") 28 | if len(packSub) == 0 { 29 | packSub = section.Get("pack-sub") 30 | } 31 | flow = section.GetMultiLineVal("flow", false) 32 | return 33 | } 34 | 35 | func SaveFlowFile(path string, flow []string, help string, abbrs string, 36 | trivial string, autoArgs string, packSub string) { 37 | 38 | meta := meta_file.CreateMetaFile(path) 39 | section := meta.GetGlobalSection() 40 | if len(help) != 0 { 41 | section.Set("help", help) 42 | } 43 | trivial = strings.TrimSpace(trivial) 44 | if len(trivial) != 0 && trivial != "0" { 45 | section.Set("trivial", trivial) 46 | } 47 | if len(abbrs) != 0 { 48 | section.Set("abbrs", abbrs) 49 | } 50 | if len(autoArgs) == 0 { 51 | autoArgs = "*" 52 | } 53 | if len(packSub) != 0 { 54 | section.Set("pack-subflow", packSub) 55 | } 56 | section.Set("args.auto", autoArgs) 57 | if len(flow) != 0 { 58 | section.SetMultiLineVal("flow", flow) 59 | } 60 | meta.Save() 61 | } 62 | -------------------------------------------------------------------------------- /pkg/mods/persist/hub_meta/hub_meta.go: -------------------------------------------------------------------------------- 1 | package hub_meta 2 | 3 | import ( 4 | "bufio" 5 | "fmt" 6 | "os" 7 | "path/filepath" 8 | "strings" 9 | ) 10 | 11 | type RepoAddr struct { 12 | Addr string 13 | Branch string 14 | } 15 | 16 | func (self RepoAddr) Str() string { 17 | if len(self.Addr) == 0 { 18 | return self.Addr 19 | } 20 | if len(self.Branch) == 0 { 21 | return self.Addr 22 | } 23 | return self.Addr + RepoAddrBranchSep + self.Branch 24 | } 25 | 26 | func ParseRepoAddr(addr string) RepoAddr { 27 | branch := "" 28 | i := strings.LastIndex(addr, RepoAddrBranchSep) 29 | if i > 0 { 30 | branch = addr[i+1:] 31 | addr = addr[:i] 32 | } 33 | return RepoAddr{addr, branch} 34 | } 35 | 36 | type RepoInfo struct { 37 | Addr RepoAddr 38 | AddReason string 39 | Path string 40 | HelpStr string 41 | OnOff string 42 | } 43 | 44 | func (self RepoInfo) IsLocal() bool { 45 | return len(self.Addr.Addr) == 0 46 | } 47 | 48 | func WriteReposInfoFile( 49 | hubRootPath string, 50 | path string, 51 | infos []RepoInfo, sep string) { 52 | 53 | os.MkdirAll(filepath.Dir(path), os.ModePerm) 54 | tmp := path + ".tmp" 55 | file, err := os.OpenFile(tmp, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644) 56 | if err != nil { 57 | panic(fmt.Errorf("[WriteReposInfoFile] open file '%s' failed: %v", tmp, err)) 58 | } 59 | defer file.Close() 60 | 61 | for _, info := range infos { 62 | _, err = fmt.Fprintf(file, "%s%s%s%s%s%s%s%s%s\n", info.Addr.Str(), sep, info.AddReason, sep, 63 | tryConvAbsPathToRelPath(hubRootPath, info.Path), sep, info.HelpStr, sep, info.OnOff) 64 | if err != nil { 65 | panic(fmt.Errorf("[WriteReposInfoFile] write file '%s' failed: %v", tmp, err)) 66 | } 67 | } 68 | file.Close() 69 | 70 | err = os.Rename(tmp, path) 71 | if err != nil { 72 | panic(fmt.Errorf("[WriteReposInfoFile] rename file '%s' to '%s' failed: %v", 73 | tmp, path, err)) 74 | } 75 | } 76 | 77 | func ReadReposInfoFile( 78 | hubRootPath string, 79 | path string, 80 | allowNotExist bool, 81 | sep string) (infos []RepoInfo, list map[string]bool) { 82 | 83 | list = map[string]bool{} 84 | 85 | file, err := os.Open(path) 86 | if err != nil { 87 | if os.IsNotExist(err) && allowNotExist { 88 | return 89 | } 90 | panic(fmt.Errorf("[ReadReposInfoFile] open file '%s' failed: %v", path, err)) 91 | } 92 | defer file.Close() 93 | 94 | scanner := bufio.NewScanner(file) 95 | scanner.Split(bufio.ScanLines) 96 | for scanner.Scan() { 97 | line := strings.Trim(scanner.Text(), "\n\r") 98 | fields := strings.Split(line, sep) 99 | if len(fields) != 5 { 100 | panic(fmt.Errorf("[ReadReposInfoFile] file '%s' line '%s' can't be parsed", 101 | path, line)) 102 | } 103 | info := RepoInfo{ 104 | ParseRepoAddr(fields[0]), 105 | fields[1], 106 | tryConvRelPathToAbsPath(hubRootPath, fields[2]), 107 | fields[3], 108 | fields[4], 109 | } 110 | infos = append(infos, info) 111 | list[info.Addr.Str()] = true 112 | } 113 | return 114 | } 115 | 116 | func tryConvAbsPathToRelPath(hubRootPath string, path string) string { 117 | if strings.HasPrefix(path, hubRootPath) { 118 | return HubRootPathMark + path[len(hubRootPath):] 119 | } 120 | return path 121 | } 122 | 123 | func tryConvRelPathToAbsPath(hubRootPath string, path string) string { 124 | if strings.HasPrefix(path, HubRootPathMark) { 125 | return hubRootPath + path[len(HubRootPathMark):] 126 | } 127 | return path 128 | } 129 | 130 | // TODO: bad func name 131 | func ExtractAddrFromList( 132 | infos []RepoInfo, 133 | findStr string) (extracted []RepoInfo, rest []RepoInfo) { 134 | 135 | if len(findStr) == 0 { 136 | return infos, nil 137 | } 138 | 139 | for i, info := range infos { 140 | findInStr := info.Addr.Str() 141 | if len(findInStr) == 0 { 142 | findInStr = info.Path 143 | } 144 | if findInStr == findStr { 145 | return []RepoInfo{info}, append(infos[:i], infos[i+1:]...) 146 | } 147 | if strings.Index(findInStr, findStr) >= 0 { 148 | extracted = append(extracted, info) 149 | } else { 150 | rest = append(rest, info) 151 | } 152 | } 153 | return 154 | } 155 | 156 | const ( 157 | RepoAddrBranchSep = "#" 158 | HubRootPathMark = ":" 159 | ) 160 | -------------------------------------------------------------------------------- /pkg/mods/persist/hub_meta/repo.go: -------------------------------------------------------------------------------- 1 | package hub_meta 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "github.com/innerr/ticat/pkg/mods/persist/meta_file" 8 | ) 9 | 10 | func ReadRepoListFromFile(selfName string, path string) (helpStr string, addrs []RepoAddr, helpStrs []string) { 11 | metas, err := meta_file.NewMetaFileEx(path) 12 | if err != nil { 13 | if os.IsNotExist(err) { 14 | return 15 | } 16 | panic(fmt.Errorf("[ReadRepoListFromFile] read mod meta file '%s' failed: %v", path, err)) 17 | } 18 | if len(metas) != 1 { 19 | panic(fmt.Errorf("[ReadRepoListFromFile] repo meta file '%s' should not be a combined file", path)) 20 | } 21 | meta := metas[0].Meta 22 | 23 | helpStr = meta.Get("help") 24 | repos := meta.GetSection("repos") 25 | if repos == nil { 26 | repos = meta.GetSection("repo") 27 | } 28 | if repos == nil { 29 | return 30 | } 31 | for _, addr := range repos.Keys() { 32 | addrs = append(addrs, ParseRepoAddr(addr)) 33 | helpStrs = append(helpStrs, repos.Get(addr)) 34 | } 35 | return 36 | } 37 | -------------------------------------------------------------------------------- /pkg/ticat/consts.go: -------------------------------------------------------------------------------- 1 | package ticat 2 | 3 | const ( 4 | SelfName string = "ticat" 5 | ListSep string = "," 6 | CmdRootDisplayName string = "" 7 | CmdBuiltinName string = "builtin" 8 | CmdBuiltinDisplayName string = "" 9 | Spaces string = "\t\n\r " 10 | AbbrsSep string = "|" 11 | EnvOpSep string = ":" 12 | SequenceSep string = ":" 13 | CmdPathSep string = "." 14 | FakeCmdPathSepSuffixs string = "/\\" 15 | CmdPathAlterSeps string = "." 16 | EnvBracketLeft string = "{" 17 | EnvBracketRight string = "}" 18 | EnvKeyValSep string = "=" 19 | EnvPathSep string = "." 20 | SysArgPrefix string = "%" 21 | EnvValDelAllMark string = "--" 22 | EnvRuntimeSysPrefix string = "sys" 23 | EnvStrsPrefix string = "strs" 24 | EnvFileName string = "bootstrap.env" 25 | ProtoSep string = "\t" 26 | ModsRepoExt string = "." + SelfName 27 | MetaExt string = "." + SelfName 28 | FlowExt string = ".tiflow" 29 | HelpExt string = ".tihelp" 30 | HubFileName string = "repos.hub" 31 | ReposFileName string = "hub.ticat" 32 | SessionEnvFileName string = "env" 33 | SessionStatusFileName string = "status" 34 | FlowTemplateBracketLeft string = "[[" 35 | FlowTemplateBracketRight string = "]]" 36 | FlowTemplateMultiplyMark string = "*" 37 | TagMark string = "@" 38 | TrivialMark string = "^" 39 | TagOutOfTheBox string = TagMark + "ready" 40 | TagProvider string = TagMark + "config" 41 | TagSelfTest string = TagMark + "selftest" 42 | EnvSnapshotExt string = ".env" 43 | CmdPathSession string = "sessions" 44 | ArgEnumSep string = "|" 45 | ) 46 | --------------------------------------------------------------------------------