├── .github
├── renovate.json
└── workflows
│ ├── trunk-check.yml
│ ├── zsh-n.yml
│ └── zunit.yml
├── .gitignore
├── .trunk
├── .gitignore
├── configs
│ └── .markdownlint.yaml
└── trunk.yaml
├── .zunit.yml
├── LICENSE
├── Makefile
├── docs
└── README.md
├── functions
├── @str-dump
├── @str-ng-match
├── @str-ng-matches
├── @str-parse-json
├── @str-read-all
├── @str-read-ini
└── @str-read-toml
├── test_input.txt
├── tests
└── main.zunit
└── zsh-string-lib.lib.zsh
/.github/renovate.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": ["config:base"]
3 | }
4 |
--------------------------------------------------------------------------------
/.github/workflows/trunk-check.yml:
--------------------------------------------------------------------------------
1 | ---
2 | name: "⭕ Trunk"
3 | on:
4 | push:
5 | branches: [main]
6 | tags: ["v*.*.*"]
7 | pull_request:
8 | types: [opened, synchronize]
9 | workflow_dispatch: {}
10 |
11 | jobs:
12 | check:
13 | name: "⚡"
14 | uses: z-shell/.github/.github/workflows/trunk.yml@main
15 |
--------------------------------------------------------------------------------
/.github/workflows/zsh-n.yml:
--------------------------------------------------------------------------------
1 | ---
2 | name: "✅ Zsh"
3 | on:
4 | push:
5 | tags: ["v*.*.*"]
6 | branches:
7 | - main
8 | - next
9 | paths:
10 | - "*.zsh"
11 | - "functions/*"
12 | pull_request:
13 | paths:
14 | - "*.zsh"
15 | - "functions/*"
16 | workflow_dispatch: {}
17 |
18 | jobs:
19 | zsh-matrix:
20 | runs-on: ubuntu-latest
21 | outputs:
22 | matrix: ${{ steps.set-matrix.outputs.matrix }}
23 | steps:
24 | - name: ⤵️ Check out code from GitHub
25 | uses: actions/checkout@v4
26 | - name: "⚡ Set matrix output"
27 | id: set-matrix
28 | run: |
29 | MATRIX="$(find . -type d -name 'doc' -prune -o -type f \( -iname '*.zsh' -o -iname '@str-*' \) -print | jq -ncR '{"include": [{"file": inputs}]}')"
30 | echo "MATRIX=${MATRIX}" >&2
31 | echo "matrix=${MATRIX}" >> $GITHUB_OUTPUT
32 | zsh-n:
33 | runs-on: ubuntu-latest
34 | needs: zsh-matrix
35 | strategy:
36 | fail-fast: false
37 | matrix: ${{ fromJSON(needs.zsh-matrix.outputs.matrix) }}
38 | steps:
39 | - name: ⤵️ Check out code from GitHub
40 | uses: actions/checkout@v4
41 | - name: "⚡ Install dependencies"
42 | run: sudo apt update && sudo apt-get install -yq zsh
43 | - name: "⚡ zsh -n: ${{ matrix.file }}"
44 | env:
45 | ZSH_FILE: ${{ matrix.file }}
46 | run: |
47 | zsh -n "${ZSH_FILE}"
48 | - name: "⚡ zcompile ${{ matrix.file }}"
49 | env:
50 | ZSH_FILE: ${{ matrix.file }}
51 | run: |
52 | zsh -fc "zcompile ${ZSH_FILE}"; rc=$?
53 | ls -al "${ZSH_FILE}.zwc"; exit "$rc"
54 |
--------------------------------------------------------------------------------
/.github/workflows/zunit.yml:
--------------------------------------------------------------------------------
1 | name: 🛡️ ZUnit
2 |
3 | on:
4 | workflow_dispatch:
5 | push:
6 | branches: [main]
7 | pull_request_target:
8 | branches: [main]
9 |
10 | jobs:
11 | build-macos:
12 | name: 🧪 Mac ZUnit CI
13 | runs-on: macos-latest
14 | steps:
15 | - uses: actions/checkout@v4
16 | - name: 📚 Molovo zunit
17 | run: |
18 | mkdir bin
19 | curl -fsSL https://raw.githubusercontent.com/zdharma/revolver/v0.2.4/revolver > bin/revolver
20 | curl -fsSL https://raw.githubusercontent.com/zdharma/color/d8f91ab5fcfceb623ae45d3333ad0e543775549c/color.zsh > bin/color
21 | curl -L https://raw.githubusercontent.com/zdharma/revolver/master/revolver > bin/revolver
22 | curl -L https://raw.githubusercontent.com/zdharma/color/master/color.zsh > bin/color
23 | git clone https://github.com/zdharma/zunit.git zunit.git
24 | cd zunit.git
25 | ./build.zsh
26 | cd ..
27 | mv ./zunit.git/zunit bin
28 | chmod u+x bin/{color,revolver,zunit}
29 | - name: ✏️ Run the test
30 | run: |
31 | export TERM="xterm-256color"
32 | export PATH="$PWD/bin:$PATH"
33 | zunit
34 |
35 | build-linux:
36 | name: 🧪 Linux Zunit CI
37 | runs-on: ubuntu-latest
38 | strategy:
39 | matrix:
40 | os: [ubuntu-latest]
41 | steps:
42 | - uses: actions/checkout@v4
43 | - name: ℹ️ Setup linux dependencies
44 | run: |
45 | sudo apt update
46 | sudo apt-get install zsh -yq
47 | - name: 📚 Molovo zunit
48 | run: |
49 | mkdir bin
50 | curl -fsSL https://raw.githubusercontent.com/zdharma/revolver/v0.2.4/revolver > bin/revolver
51 | curl -fsSL https://raw.githubusercontent.com/zdharma/color/d8f91ab5fcfceb623ae45d3333ad0e543775549c/color.zsh > bin/color
52 | curl -L https://raw.githubusercontent.com/zdharma/revolver/master/revolver > bin/revolver
53 | curl -L https://raw.githubusercontent.com/zdharma/color/master/color.zsh > bin/color
54 | git clone https://github.com/zdharma/zunit.git zunit.git
55 | cd zunit.git
56 | ./build.zsh
57 | cd ..
58 | mv ./zunit.git/zunit bin
59 | chmod u+x bin/{color,revolver,zunit}
60 | - name: ✏️ Run the test
61 | run: |
62 | export TERM="xterm-256color"
63 | export PATH="$PWD/bin:$PATH"
64 | zunit
65 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Zsh compiled script + zrecompile backup
2 | *.zwc
3 | *.zwc.old
4 |
5 | # Zsh completion-optimization dumpfile
6 | *zcompdump*
7 |
8 | # Zsh zcalc history
9 | .zcalc_history
10 |
11 | # A popular plugin manager's files
12 | ._zplugin
13 | .zplugin_lstupd
14 |
15 | # zdharma/zshelldoc tool's files
16 | zsdoc/data
17 |
18 | # robbyrussell/oh-my-zsh/plugins/per-directory-history plugin's files
19 | # (when set-up to store the history in the local directory)
20 | .directory_history
21 |
22 | # MichaelAquilina/zsh-autoswitch-virtualenv plugin's files
23 | # (for Zsh plugins using Python)
24 | .venv
25 |
26 | # Zunit tests' output
27 | /tests/_output/*
28 | !/tests/_output/.gitkeep
29 |
--------------------------------------------------------------------------------
/.trunk/.gitignore:
--------------------------------------------------------------------------------
1 | *out
2 | *logs
3 | *actions
4 | *notifications
5 | plugins
6 | user_trunk.yaml
7 | user.yaml
8 |
--------------------------------------------------------------------------------
/.trunk/configs/.markdownlint.yaml:
--------------------------------------------------------------------------------
1 | # Autoformatter friendly markdownlint config (all formatting rules disabled)
2 | default: true
3 | blank_lines: false
4 | bullet: false
5 | html: false
6 | indentation: false
7 | line_length: false
8 | spaces: false
9 | url: false
10 | whitespace: false
11 |
--------------------------------------------------------------------------------
/.trunk/trunk.yaml:
--------------------------------------------------------------------------------
1 | version: 0.1
2 | cli:
3 | version: 1.3.1
4 | plugins:
5 | sources:
6 | - id: trunk
7 | ref: v0.0.8
8 | uri: https://github.com/trunk-io/plugins
9 | lint:
10 | enabled:
11 | - markdownlint@0.33.0
12 | - actionlint@1.6.22
13 | - gitleaks@8.15.2
14 | - git-diff-check
15 | - prettier@2.8.3
16 | runtimes:
17 | enabled:
18 | - go@1.18.3
19 | - node@18.12.1
20 | actions:
21 | enabled:
22 | - trunk-announce
23 | - trunk-check-pre-push
24 | - trunk-fmt-pre-commit
25 | - trunk-upgrade-available
26 |
--------------------------------------------------------------------------------
/.zunit.yml:
--------------------------------------------------------------------------------
1 | tap: false
2 | directories:
3 | tests: tests
4 | time_limit: 0
5 | fail_fast: false
6 | allow_risky: false
7 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | This software is licensed under MIT.
2 |
3 | MIT License
4 | -----------
5 |
6 | Permission is hereby granted, free of charge, to any person obtaining a copy
7 | of this software and associated documentation files (the "Software"), to deal
8 | in the Software without restriction, including without limitation the rights
9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | copies of the Software, and to permit persons to whom the Software is
11 | furnished to do so, subject to the following conditions:
12 |
13 | The above copyright notice and this permission notice shall be included in
14 | all copies or substantial portions of the Software.
15 |
16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22 | THE SOFTWARE.
23 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | check: test
2 |
3 | test:
4 | @command -v zunit >/dev/null && \
5 | { zunit; ((1)); } || \
6 | echo "Please install zunit to run the tests (https://github.com/zdharma/zunit)"
7 |
--------------------------------------------------------------------------------
/docs/README.md:
--------------------------------------------------------------------------------
1 |
Zsh String Lib
2 |
3 | # Introduction
4 |
5 | A string library for Zsh. Its founding function was parsing of JSON.
6 |
7 | ## List Of The Functions
8 |
9 | ### @str-parse-json
10 |
11 | Parses the buffer (`$1`) with JSON and returns:
12 |
13 | 1. Fields for the given key (`$2`) in the given hash (`$3`).
14 | 2. The hash looks like follows:
15 |
16 | ```txt
17 | 1/1 → strings at the level 1 of the 1st object
18 | 1/2 → strings at the level 1 of the 2nd object
19 | …
20 | 2/1 → strings at 2nd level of the 1st object
21 | …
22 | ```
23 |
24 | The strings are parseable with `"${(@Q)${(@z)value}"`, i.e.:
25 | they're concatenated and quoted strings found in the JSON.
26 |
27 | Example:
28 |
29 | ```json
30 | {
31 | "zi-ices": {
32 | "default": {
33 | "wait": "1",
34 | "lucid": "",
35 | "as": "program",
36 | "pick": "fzy",
37 | "make": ""
38 | },
39 | "bgn": {
40 | "wait": "1",
41 | "lucid": "",
42 | "as": "null",
43 | "make": "",
44 | "sbin": "fzy;contrib/fzy-*"
45 | }
46 | }
47 | }
48 | ```
49 |
50 | Will result in:
51 |
52 | ```zsh
53 | local -A Strings
54 | Strings[1/1]="zi-ices"
55 | Strings[2/1]="default $'\0'--object--$'\0' bgn $'\0'--object--$'\0'"
56 | Strings[3/1]='wait 1 lucid \ as program pick fzy make \ '
57 | Strings[3/2]='wait 1 lucid \ as null make \ sbin fzy\;contrib/fzy-\*'
58 | ```
59 |
60 | So that when you e.g.: expect a key `bgn` but don't know at which
61 | position, you can do:
62 |
63 | ```zsh
64 | local -A Strings
65 | @str-parse-json "$json" "zi-ices" Strings
66 |
67 | integer pos
68 | # (I) flag returns index at which the `bgn' string
69 | # has been found in the array – the result of the
70 | # (z)-split of the Strings[2/1] string
71 | pos=${${(@Q)${(@z)Strings[2/1]}}[(I)bgn]}
72 | if (( pos )) {
73 | local -A ices
74 | ices=( "${(@Q)${(@z)Strings[3/$(( (pos+1) / 2 ))]}}" )
75 | # Use the `ices' hash holding the values of the `bgn' object
76 | …
77 | }
78 | ```
79 |
80 | Note that the `$'\0'` is correctly dequoted by `Q` flag into the null byte.
81 |
82 | Arguments:
83 |
84 | 1. The buffer with JSON.
85 | 2. The key in the JSON that should be mapped to the result (i.e.: it's possible
86 | to map only a subset of the input). It must be the first key in the object to
87 | map.
88 | 3. The name of the output hash parameter.
89 |
90 | ### @str-read-all
91 |
92 | Consumes whole data from given file descriptor and stores the string under the
93 | given (`$2`) parameter, which is `REPLY` by default.
94 |
95 | The reason to create this function is speed – it's much faster than `read -d ''`.
96 |
97 | It can try hard to read the whole data by retrying multiple times (`10` by
98 | default) and sleeping before each retry (not done by default).
99 |
100 | Arguments:
101 |
102 | 1. File descriptor (a number; use `1` for stdin) to be read from.
103 | 2. Name of output variable (default: `REPLY`).
104 | 3. Numer of retries (default: `10`).
105 | 4. Sleep time after each retry (a float; default: `0`).
106 |
107 | Example:
108 |
109 | ```zsh
110 | exec {FD}< =( cat /etc/motd )
111 | @str-read-all $FD
112 | print -r -- $REPLY
113 | …
114 | ```
115 |
116 | ### @str-ng-match
117 |
118 | Returns a non-greedy match of the given pattern (`$2`) in the given string
119 | (`$1`).
120 |
121 | 1. The string to match in.
122 | 2. The pattern to match in the string.
123 |
124 | Return value:
125 |
126 | - `$REPLY` – the matched string, if found,
127 | - return code: `0` if there was a match found, otherwise `1`.
128 |
129 | Example:
130 |
131 | ```zsh
132 | if @str-ng-match "abb" "a*b"; then
133 | print -r -- $REPLY
134 | fi
135 | Output: ab
136 | ```
137 |
138 | ### @str-ng-matches
139 |
140 | Returns all non-greedy matches of the given pattern in the given list of
141 | strings.
142 |
143 | Input:
144 |
145 | - `$1` … `$n-1` - the strings to match in,
146 | - `$n` - the pattern to match in the strings.
147 |
148 | Return value:
149 |
150 | - `$reply` – contains all the matches,
151 | - `$REPLY` - holds the first match,
152 | - return code: `0` if there was any match found, otherwise `1`.
153 |
154 | Example:
155 |
156 | ```zsh
157 | arr=( a1xx ayy a2xx )
158 | if @str-ng-matches ${arr[@]} "a*x"; then
159 | print -rl -- $reply
160 | fi
161 |
162 | Outout:
163 | a1x
164 | a2x
165 | ```
166 |
167 | ### @str-read-ini
168 |
169 | Reads an INI file.
170 |
171 | Arguments:
172 |
173 | 1. Path to the ini file to parse.
174 | 2. Name of output hash (`INI` by default).
175 | 3. Prefix for keys in the hash (can be empty).
176 |
177 | Writes to given hash under keys built in following way: `${3}_field`.
178 | Values are the values from the ini file.
179 |
180 | ### @str-read-toml
181 |
182 | Reads a TOML file with support for single-level array.
183 |
184 | 1. Path to the TOML file to parse.
185 | 2. Name of output hash (`TOML` by default).
186 | 3. Prefix for keys in the hash (can be empty).
187 |
188 | Writes to given hash under keys built in following way: `${3}_field`.
189 | Values are the values from the TOML file.
190 |
191 | The values can be quoted and concatenated strings if they're an array. For
192 | example:
193 |
194 | ```ini
195 | [sec]
196 | array = [ val1, "value 2", value&3 ]
197 | ```
198 |
199 | Then the fields of the hash will be:
200 |
201 | ```zsh
202 | TOML[_array]="val1 value\ 2 value\&3"
203 | ```
204 |
205 | To retrieve the array stored in such way, use the substitution
206 | `"${(@Q)${(@z)TOML[_array]}}"`:
207 |
208 | ```zsh
209 | local -a array
210 | array=( "${(@Q)${(@z)TOML[_array]}}" )
211 | ```
212 |
213 | (The substitution first splits the input string as if Zsh would split it on the
214 | command line – with the `(z)` flag, and then removes one level of quoting with
215 | the `(Q)` flag).
216 |
217 | ### @str-dump
218 |
219 | Dumps the contents of the variable, whether it's being a scalar, an array or
220 | a hash. The contents of the hash are sorted on the keys numerically, i.e.: by
221 | using `(on)` flags.
222 |
223 | An option `-q` can be provided: it'll enable quoting of the printed data with
224 | the `q`-flag (i.e.: backslash quoting).
225 |
226 | Basically, the function Is an alternative to `declare -p`, with a different
227 | output format, more dump-like.
228 |
229 | Arguments:
230 |
231 | 1. The name of the variable of which contents should be dumped.
232 |
233 | Example:
234 |
235 | ```zsh
236 | array=( "" "a value" "test" )
237 | @str-dump -q array
238 | ```
239 |
240 | Output:
241 |
242 | ```sh
243 | ''
244 | a\ value
245 | test
246 | ```
247 |
248 | ```zsh
249 | typeset -A hash=( "a key" "a value" key value )
250 | @str-dump -q hash
251 | ```
252 |
253 | Output:
254 |
255 | ```sh
256 | a\ key: a\ value
257 | key: value
258 | ```
259 |
--------------------------------------------------------------------------------
/functions/@str-dump:
--------------------------------------------------------------------------------
1 | # Copyright (c) 2018 Sebastian Gniazdowski
2 | # Copyright (c) 2021 Salvydas Lukosius
3 | #
4 | # @str-dump
5 | #
6 | # $1 - name of the variable of which contents should be dumped
7 | #
8 | # Dumps the contents of the variable, whether it's being a scalar,
9 | # an array or a hash. The contents of the hash are sorted on the
10 | # keys numerically, i.e.: by using `(on)` flags.
11 | #
12 | # An option -q can be provided: it'll enable quoting of the printed
13 | # data with the q-flag (i.e.: backslash quoting).
14 | #
15 | # Example:
16 | # array=( "" "a value" "test" )
17 | # @str-dump array
18 | #
19 | # Output:
20 | # ''
21 | # a\ value
22 | # test
23 | #
24 | # typeset -A hash=( "a key" "a value" key value )
25 | # @str-dump -q hash
26 | #
27 | # Output:
28 | # a\ key: a\ value
29 | # key: value
30 |
31 | @str-dump() {
32 | if [[ $1 = -q ]] {
33 | integer quote=1
34 | shift
35 | } else {
36 | integer quote=0
37 | }
38 |
39 | if [[ $1 = -- ]] { shift; }
40 |
41 | local __var_name="$1"
42 |
43 | (( ${(P)+__var_name} )) || {
44 | print "Error: the parameter \`$__var_name' doesn't exist"
45 | return 1
46 | }
47 | case ${(Pt)__var_name} in
48 | (*array*)
49 | (( quote )) && \
50 | { print -rl -- "${(@qP)__var_name}"; ((1)); } || \
51 | print -rl -- "${(@P)__var_name}"
52 | ;;
53 | (*association*)
54 | # The double kv usage is because the behavior
55 | # changed in a Zsh version
56 | local -a keys
57 | local key access_string
58 | keys=( "${(@kon)${(@Pk)__var_name}}" )
59 | for key in "${keys[@]}"; do
60 | access_string="${__var_name}[$key]"
61 | (( quote )) && { print -r -- "${(q)key}: ${(qP)access_string}"; ((1)); } || print -r -- "$key: ${(P)access_string}"
62 | done
63 | ;;
64 | (*)
65 | (( quote )) && { print -r -- "${(qP)__var_name}"; ((1)); } || print -r -- "${(P)__var_name}"
66 | ;;
67 | esac
68 |
69 | return 0
70 | }
71 |
--------------------------------------------------------------------------------
/functions/@str-ng-match:
--------------------------------------------------------------------------------
1 | # Copyright (c) 2018 Sebastian Gniazdowski
2 | # Copyright (c) 2021 Salvydas Lukosius
3 | #
4 | # @str-ng-match
5 | #
6 | # Returns a non-greedy match of the given pattern ($2)
7 | # in the given string ($1).
8 | #
9 | # $1 - the string to match in
10 | # $2 - the pattern to match in the string
11 | #
12 | # Example:
13 | #
14 | # if @str-ng-match "abb" "a*b"; then
15 | # print $REPLY
16 | # fi
17 | # Output: ab
18 | @str-ng-match() {
19 | local str="$1" pat="$2" retval=1
20 | : "${(S)str/(#b)(${~pat})/${retval::=0}}"
21 | REPLY="${match[1]}"
22 | return "$retval"
23 | }
24 |
--------------------------------------------------------------------------------
/functions/@str-ng-matches:
--------------------------------------------------------------------------------
1 | # Copyright (c) 2018 Sebastian Gniazdowski
2 | # Copyright (c) 2021 Salvydas Lukosius
3 | #
4 | # @str-ng-matches
5 | #
6 | # Returns all non-greedy matches of the given pattern ($2)
7 | # in the given string ($1).
8 | #
9 | # $1 … n-1 - the strings to match in
10 | # $n - the pattern to match in the strings
11 | #
12 | # Return value: $reply – contains all the matches
13 | # $REPLY - holds the first match
14 | # $?: 0 if there was any match found, otherwise 1
15 | #
16 | # Example:
17 | # arr=( a1xx ayy a2xx )
18 | # if @str-ng-matches ${arr[@]} "a*x"; then
19 | # print -rl $reply
20 | # fi
21 | #
22 | # Output:
23 | # a1x
24 | # a2x
25 |
26 | @str-ng-matches() {
27 | local pat="${@[${#}]}" retval=1
28 | local -a input
29 | input=( "${@[1,${#}-1]}" ) reply=() match=()
30 | : "${(S)input[@]//(#b)(${~pat})/${reply[${#reply}+1]::=${match[1]}}${retval::=0}}"
31 | REPLY="${match[1]}"
32 | return $retval
33 | }
--------------------------------------------------------------------------------
/functions/@str-parse-json:
--------------------------------------------------------------------------------
1 | # @str-parse-json
2 | #
3 | # Parses the buffer ($1) with JSON and returns:
4 | #
5 | # 1. Fields for the given key ($2) in the given hash ($3)
6 | # 2. The hash looks like follows:
7 | # 1/1 → strings at the level 1 of the 1st object
8 | # 1/2 → strings at the level 1 of the 2nd object
9 | # …
10 | # 2/1 → strings at 2nd level of the 1st object
11 | # …
12 | #
13 | # The strings are parseable with "${(@Q)${(@z)value}", i.e.:
14 | # they're concatenated and quoted strings found in the JSON.
15 | #
16 | # Example:
17 | #
18 | # {"zi-ices":{
19 | # "default":{
20 | # "wait":"1",
21 | # "lucid":"",
22 | # "as":"program",
23 | # "pick":"fzy",
24 | # "make":"",
25 | # },
26 | # "bgn":{
27 | # "wait":"1",
28 | # "lucid":"",
29 | # "as":"null",
30 | # "make":"",
31 | # "sbin":"fzy;contrib/fzy-*"
32 | # }
33 | # }}
34 | #
35 | # Will result in:
36 | #
37 | # local -A Strings
38 | # Strings[1/1]='zi-ices'
39 | # Strings[2/1]='default bgn'
40 | # Strings[3/1]='wait 1 lucid \ as program pick fzy make \ '
41 | # Strings[3/2]='wait 1 lucid \ as null make \ sbin fzy\;contrib/fzy-\*'
42 | #
43 | # So that when you e.g.: expect a key `bgn' but don't know at which
44 | # position, you can do:
45 | #
46 | # local -A Strings
47 | # @str-parse-json "$json" "zi-ices" Strings
48 | #
49 | # integer pos
50 | # pos=${${(@Q)${(@z)Strings[2/1]}}[(I)bgn]}
51 | # if (( pos )) {
52 | # local -A ices
53 | # ices=( "${(@Q)${(@z)Strings[3/$pos]}}" )
54 | # # Use the `ices' hash holding the values of the `bgn' object
55 | # …
56 | # }
57 | #
58 | # $1 - the buffer with JSON
59 | # $2 - the key in the JSON that should be mapped to the
60 | # result (i.e.: you can map subset of the input)
61 | # $3 - the name of the output hash parameter
62 |
63 | @str-parse-json() {
64 | emulate -LR zsh -o extendedglob -o warncreateglobal -o typesetsilent
65 |
66 | local -A __pos_to_level __level_to_pos __pair_map \
67 | __final_pairs __Strings __Counts
68 | local __input=$1 __workbuf=$1 __key=$2 __varname=$3 \
69 | __style __quoting
70 | integer __nest=${4:-1} __idx=0 __pair_idx __level=0 \
71 | __start __end __sidx=1 __had_quoted_value=0
72 | local -a match mbegin mend __pair_order
73 |
74 | (( ${(P)+__varname} )) || typeset -gA "$__varname"
75 |
76 | __pair_map=( "(" ")" "{" "}" "[" "]" )
77 | while [[ $__workbuf = (#b)[^"{}()[]\\\"'":,]#((["({[]})\"'":,])|[\\](*))(*) ]]; do
78 | [[ -n ${match[3]} ]] && {
79 | __idx+=${mbegin[1]}
80 |
81 | [[ $__quoting = \' ]] && \
82 | { __workbuf=${match[3]}; } || \
83 | { __workbuf=${match[3]:1}; (( ++ __idx )); }
84 |
85 | } || {
86 | __idx+=${mbegin[1]}
87 | [[ -z $__quoting ]] && {
88 | if [[ ${match[1]} = ["({["] ]]; then
89 | __Strings[$__level/${__Counts[$__level]}]+=" $'\0'--object--$'\0'"
90 | __pos_to_level[$__idx]=$(( ++ __level ))
91 | __level_to_pos[$__level]=$__idx
92 | (( __Counts[$__level] += 1 ))
93 | __sidx=__idx+1
94 | __had_quoted_value=0
95 | elif [[ ${match[1]} = ["]})"] ]]; then
96 | (( !__had_quoted_value )) && \
97 | __Strings[$__level/${__Counts[$__level]}]+=" ${(q)__input[__sidx,__idx-1]//((#s)[[:blank:]]##|([[:blank:]]##(#e)))}"
98 | __had_quoted_value=1
99 | if (( __level > 0 )); then
100 | __pair_idx=${__level_to_pos[$__level]}
101 | __pos_to_level[$__idx]=$(( __level -- ))
102 | [[ ${__pair_map[${__input[__pair_idx]}]} = ${__input[__idx]} ]] && {
103 | __final_pairs[$__idx]=$__pair_idx
104 | __final_pairs[$__pair_idx]=$__idx
105 | __pair_order+=( $__idx )
106 | }
107 | else
108 | __pos_to_level[$__idx]=-1
109 | fi
110 | fi
111 | }
112 |
113 | [[ ${match[1]} = \" && $__quoting != \' ]] && \
114 | if [[ $__quoting = '"' ]]; then
115 | __Strings[$__level/${__Counts[$__level]}]+=" ${(q)__input[__sidx,__idx-1]}"
116 | __quoting=""
117 | else
118 | __had_quoted_value=1
119 | __sidx=__idx+1
120 | __quoting='"'
121 | fi
122 |
123 | [[ ${match[1]} = , && -z $__quoting ]] && \
124 | {
125 | (( !__had_quoted_value )) && \
126 | __Strings[$__level/${__Counts[$__level]}]+=" ${(q)__input[__sidx,__idx-1]//((#s)[[:blank:]]##|([[:blank:]]##(#e)))}"
127 | __sidx=__idx+1
128 | __had_quoted_value=0
129 | }
130 |
131 | [[ ${match[1]} = : && -z $__quoting ]] && \
132 | {
133 | __had_quoted_value=0
134 | __sidx=__idx+1
135 | }
136 |
137 | [[ ${match[1]} = \' && $__quoting != \" ]] && \
138 | if [[ $__quoting = "'" ]]; then
139 | __Strings[$__level/${__Counts[$__level]}]+=" ${(q)__input[__sidx,__idx-1]}"
140 | __quoting=""
141 | else
142 | __had_quoted_value=1
143 | __sidx=__idx+1
144 | __quoting="'"
145 | fi
146 |
147 | __workbuf=${match[4]}
148 | }
149 | done
150 |
151 | local __text value __found
152 | integer __pair_a __pair_b
153 | for __pair_a ( "${__pair_order[@]}" ) {
154 | __pair_b="${__final_pairs[$__pair_a]}"
155 | __text="${__input[__pair_b,__pair_a]}"
156 | if [[ $__text = [[:space:]]#\{[[:space:]]#[\"\']${__key}[\"\']* ]]; then
157 | if (( __nest != 2 )) {
158 | __found="$__text"
159 | break
160 | }
161 | fi
162 | }
163 |
164 | if [[ -n $__found && $__nest -lt 2 ]] {
165 | @str-parse-json "$__found" "$__key" "$__varname" 2
166 | }
167 |
168 | if (( __nest == 2 )) {
169 | : ${(PAA)__varname::="${(kv)__Strings[@]}"}
170 | }
171 | }
172 |
--------------------------------------------------------------------------------
/functions/@str-read-all:
--------------------------------------------------------------------------------
1 | # Copyright (c) 2018 Sebastian Gniazdowski
2 | # Copyright (c) 2021 Salvydas Lukosius
3 | #
4 | # @str-read-all
5 | #
6 | # Consumes whole data from given file descriptor and
7 | # stores the string under the given ($2) parameter,
8 | # which is REPLY by default.
9 | #
10 | # It can try hard to read the whole data by retrying
11 | # multiple times (10 by default) and sleeping before
12 | # each retry (doesn't do this by default).
13 | #
14 | # Other advantage over read -d '' is performance: the
15 | # sysread based implementation is much faster.
16 | #
17 | # $1 - file descriptor (a number) to be read from
18 | # $2 - name of output variable (default: REPLY)
19 | # $3 - numer of retries (default: 10)
20 | # $4 - sleep time after each retry (a float; default: 0)
21 | #
22 | # Inspired by:
23 | # https://github.com/okdana/shu2/blob/master/fn/shu:io:read
24 |
25 | @str-read-all() {
26 | emulate -LR zsh -o extendedglob -o warncreateglobal -o typesetsilent
27 |
28 | local __in_fd=${1:-0} __out_var=${2:-REPLY}
29 | local -a __tmp
30 | integer __ret=1 __repeat=${3:-10} __tmp_size=0
31 | float __sleept=${4:-0}
32 |
33 | while (( 1 )) {
34 | sysread -s 65535 -i "$__in_fd" '__tmp[ __tmp_size + 1 ]'
35 | (( ( __ret = $? ) == 0 )) && (( ++ __tmp_size ))
36 | (( __ret == 5 )) && {
37 | __ret=0
38 | (( -- __repeat == 0 )) && break
39 | (( __sleept )) && LANG=C sleep "$__sleept"
40 | }
41 | }
42 |
43 | : ${(P)__out_var::="${(j::)__tmp}"}
44 |
45 | return __ret
46 | }
--------------------------------------------------------------------------------
/functions/@str-read-ini:
--------------------------------------------------------------------------------
1 | # Copyright (c) 2018 Sebastian Gniazdowski
2 | # Copyright (c) 2021 Salvydas Lukosius
3 | #
4 | # @str-read-ini
5 | #
6 | # $1 - path to the ini file to parse
7 | # $2 - name of output hash (INI by default)
8 | # $3 - prefix for keys in the hash (can be empty)
9 | #
10 | # Writes to given hash under keys built in following way: ${3}_field.
11 | # Values are the values from the ini file.
12 |
13 | @str-read-ini() {
14 | local __ini_file="$1" __out_hash="${2:-INI}" __key_prefix="$3"
15 | local IFS='' __line __cur_section="void" __access_string
16 | local -a match mbegin mend
17 |
18 | [[ ! -r "$__ini_file" ]] && { builtin print -r "@str-read-ini: an ini file is unreadable ($__ini_file)"; return 1; }
19 | (( ${(P)+__out_hash} )) || typeset -gA "$__out_hash"
20 |
21 | while read -r -t 1 __line; do
22 | if [[ "$__line" = [[:blank:]]#\;* ]]; then
23 | continue
24 | elif [[ "$__line" = (#b)[[:blank:]]#\[([^\]]##)\][[:blank:]]# ]]; then
25 | __cur_section="${match[1]}"
26 | elif [[ "$__line" = (#b)[[:blank:]]#([^[:blank:]=]##)[[:blank:]]#[=][[:blank:]]#(*) ]]; then
27 | match[2]="${match[2]%"${match[2]##*[! $'\t']}"}" # remove trailing whitespace
28 | __access_string="${__out_hash}[${__key_prefix}<$__cur_section>_${match[1]}]"
29 | : "${(P)__access_string::=${match[2]}}"
30 | fi
31 | done < "$__ini_file"
32 |
33 | return 0
34 | }
35 |
--------------------------------------------------------------------------------
/functions/@str-read-toml:
--------------------------------------------------------------------------------
1 | # Copyright (c) 2018 Sebastian Gniazdowski
2 | # Copyright (c) 2021 Salvydas Lukosius
3 | #
4 | # @str-read-toml
5 | #
6 | # Reads a TOML file with support for single-level array.
7 | #
8 | # $1 - path to the toml file to parse
9 | # $2 - name of output hash (TOML by default)
10 | # $3 - prefix for keys in the hash (can be empty)
11 | #
12 | # Writes to given hash under keys built in following way: ${3}_field.
13 | # Values are values from toml file. The values can be quoted and concatenated
14 | # strings if they're an array. For example:
15 | #
16 | # [sec]
17 | # array = [ val1, "value 2", value&3 ]
18 | #
19 | # Then the fields of the hash will be:
20 | # TOML[_array]="val1 value\ 2 value\&3"
21 | #
22 | # To retrieve the array stored in such way, use the substitution
23 | # "${(@Q)${(@z)TOML[_array]}}":
24 | #
25 | # local -a array
26 | # array=( "${(@Q)${(@z)TOML[_array]}}" )
27 | #
28 |
29 | @str-read-toml() {
30 | local __toml_file="$1" __out_hash="${2:-TOML}" __key_prefix="$3"
31 | local IFS='' __line __cur_section="void" __access_string REPLY
32 | local -a match mbegin mend
33 |
34 | # Don't leak any helper functions
35 | typeset -g tomlef
36 | tomlef=( ${(k)functions} )
37 | trap "unset -f -- \"\${(k)functions[@]:|tomlef}\" &>/dev/null; unset tomlef" EXIT
38 | trap "unset -f -- \"\${(k)functions[@]:|tomlef}\" &>/dev/null; unset tomlef; return 1" INT
39 |
40 | .str-toml-parse-array() {
41 | local -A Strings
42 | @str-parse-json "{\"input\":$1}" input Strings
43 | if [[ -n "${Strings[2/1]}" ]]; then
44 | REPLY="${Strings[2/1]# }"
45 | return 0
46 | else
47 | REPLY=""
48 | return 1
49 | fi
50 | }
51 |
52 | [[ ! -r "$__toml_file" ]] && { builtin print -r "@str-read-toml: an toml file is unreadable ($__toml_file)"; return 1; }
53 | (( ${(P)+__out_hash} )) || typeset -gA "$__out_hash"
54 |
55 | while read -r -t 1 __line; do
56 | if [[ "$__line" = [[:blank:]]#\;* ]]; then
57 | continue
58 | elif [[ "$__line" = (#b)[[:blank:]]#\[([^\]]##)\][[:blank:]]# ]]; then
59 | __cur_section="${match[1]}"
60 | elif [[ "$__line" = (#b)[[:blank:]]#([^[:blank:]=]##)[[:blank:]]#[=][[:blank:]]#(*) ]]; then
61 | match[2]="${match[2]%"${match[2]##*[! $'\t']}"}" # remove trailing whitespace
62 | .str-toml-parse-array "${match[2]}" && match[2]="$REPLY"
63 | __access_string="${__out_hash}[${__key_prefix}<$__cur_section>_${match[1]}]"
64 | : "${(P)__access_string::=${match[2]}}"
65 | fi
66 | done < "$__toml_file"
67 |
68 | return 0
69 | }
--------------------------------------------------------------------------------
/test_input.txt:
--------------------------------------------------------------------------------
1 | [1]
2 | 2=[ 3, "4", '&5' ]
3 | [6]
4 | 7=[ 8, "9", '&10' ]
5 | [11]
6 | 12=[ 13, "14", '&15' ]
7 | [11]
8 | 12=13, "14", '15'
9 |
--------------------------------------------------------------------------------
/tests/main.zunit:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env zunit
2 | @setup {
3 | load "../zsh-string-lib.lib.zsh"
4 | }
5 |
6 | @test 'JSON parser' {
7 |
8 | local -A Strings
9 | evl @str-parse-json "{'a':{'b':'1'}}" a Strings
10 |
11 | assert "$state" same_as "0"
12 | assert "$output" same_as ""
13 | assert "$Strings[2/1]" same_as " b 1"
14 | }
15 |
16 | @test 'read-all' {
17 |
18 | integer FD=13371337 i
19 | local REPLY
20 | local -a reply
21 | command rm -f test_input.txt
22 | for i ( {1..30000} ) {
23 | print $i >>! test_input.txt
24 | }
25 | exec {FD}< <(command cat test_input.txt)
26 |
27 | noglob evl @str-read-all $FD \; ret="\$?" \; reply=( "\${(@f)REPLY}" ) \; return \$ret
28 |
29 | assert "$state" same_as "0"
30 | assert "$output" same_as ""
31 | assert "$reply[1]" same_as "1"
32 | assert "$reply[10]" same_as "10"
33 | assert "$reply[100]" same_as "100"
34 | assert "$reply[1000]" same_as "1000"
35 | assert "$reply[10000]" same_as "10000"
36 | assert "$reply[20000]" same_as "20000"
37 | assert "$reply[30000]" same_as "30000"
38 | assert "$reply[30001]" same_as ""
39 | }
40 |
41 | @test 'ng-match' {
42 | evl @str-ng-match "abb" "a*b"
43 |
44 | assert "$state" same_as "0"
45 | assert "$output" same_as ""
46 | assert "$REPLY" same_as "ab"
47 | }
48 |
49 | @test 'ng-matches' {
50 | arr=( a1xx ayy a2xx )
51 | evl @str-ng-matches ${arr[@]} "a*x"
52 |
53 | assert "$state" same_as "0"
54 | assert "$output" same_as ""
55 | assert "$reply[1]" same_as "a1x"
56 | assert "$reply[2]" same_as "a2x"
57 | assert "$reply[3]" same_as ""
58 | }
59 | @test 'read-ini' {
60 | integer i j k
61 | command rm -f test_input.txt
62 | for i j k ( {1..15} ) {
63 | print "[$i]\n$j=$k" >>! test_input.txt
64 | }
65 |
66 | evl @str-read-ini test_input.txt
67 | evl @str-read-ini test_input.txt INI my_
68 |
69 | assert "$state" same_as "0"
70 | assert "$output" same_as ""
71 | assert "$INI[<1>_2]" same_as "3"
72 | assert "$INI[<4>_5]" same_as "6"
73 | assert "$INI[<7>_8]" same_as "9"
74 | assert "$INI[<10>_11]" same_as "12"
75 | assert "$INI[<13>_14]" same_as "15"
76 | assert "$INI[my_<1>_2]" same_as "3"
77 | assert "$INI[my_<4>_5]" same_as "6"
78 | assert "$INI[my_<7>_8]" same_as "9"
79 | assert "$INI[my_<10>_11]" same_as "12"
80 | assert "$INI[my_<13>_14]" same_as "15"
81 | assert "${#INI}" same_as "10"
82 | }
83 |
84 | @test 'read-toml' {
85 | integer i j k l m
86 | command rm -f test_input.txt
87 | for i j k l m ( {1..15} ) {
88 | print "[$i]\n$j=[ $k, \"$l\", '&$m' ]" >>! test_input.txt
89 | }
90 | print "[$i]\n$j=$k, \"$l\", '$m'" >>! test_input.txt
91 |
92 | evl @str-read-toml test_input.txt
93 | evl @str-read-toml test_input.txt TOML my_
94 |
95 | assert "$state" same_as "0"
96 | assert "$output" same_as ""
97 | assert "$TOML[my_<1>_2]" same_as "3 4 \\&5"
98 | assert "$TOML[my_<6>_7]" same_as "8 9 \\&10"
99 | assert "$TOML[my_<11>_12]" same_as "13, \"14\", '15'"
100 | assert "$TOML[<1>_2]" same_as "3 4 \\&5"
101 | assert "$TOML[<6>_7]" same_as "8 9 \\&10"
102 | assert "$TOML[<11>_12]" same_as "13, \"14\", '15'"
103 | assert "${#TOML}" same_as "6"
104 | }
--------------------------------------------------------------------------------
/zsh-string-lib.lib.zsh:
--------------------------------------------------------------------------------
1 | # Standardized $0 Handling
2 | # https://wiki.zshell.dev/community/zsh_plugin_standard#zero-handling
3 | 0="${ZERO:-${${0:#$ZSH_ARGZERO}:-${(%):-%N}}}"
4 | 0="${${(M)0:#/*}:-$PWD/$0}"
5 |
6 | # Functions directory
7 | # https://wiki.zshell.dev/community/zsh_plugin_standard#funtions-directory
8 | if [[ $PMSPEC != *f* ]] {
9 | fpath+=( "${0:h}/functions" )
10 | }
11 |
12 | zmodload zsh/system 2>/dev/null
13 |
14 | # API-like functions
15 | # https://wiki.zshell.dev/community/zsh_plugin_standard#the-proposed-function-name-prefixes
16 | autoload -Uz \
17 | @str-parse-json \
18 | @str-read-all \
19 | @str-ng-match \
20 | @str-ng-matches \
21 | @str-read-ini \
22 | @str-read-toml \
23 | @str-dump
24 |
--------------------------------------------------------------------------------