├── .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 | --------------------------------------------------------------------------------