├── .github ├── FUNDING.yml ├── build ├── run-tests.sh └── workflows │ ├── codeql-analysis.yml │ ├── pull_request.yml │ ├── push.yml │ └── release.yml ├── .gitignore ├── LICENSE ├── README.md ├── calc ├── FUZZING.md ├── doc.go ├── evaluator.go ├── evaluator_test.go ├── fuzz_test.go ├── lexer.go └── lexer_test.go ├── chooseui └── chooseui.go ├── cmd_calc.go ├── cmd_choose_file.go ├── cmd_choose_stdin.go ├── cmd_chronic.go ├── cmd_collapse.go ├── cmd_comments.go ├── cmd_cpp.go ├── cmd_env_template.go ├── cmd_env_template.tmpl ├── cmd_env_template_helpers.go ├── cmd_exec_stdin.go ├── cmd_expect.go ├── cmd_feeds.go ├── cmd_find.go ├── cmd_fingerd.go ├── cmd_html2text.go ├── cmd_http_get.go ├── cmd_httpd.go ├── cmd_ips.go ├── cmd_markdown_toc.go ├── cmd_password.go ├── cmd_rss.go ├── cmd_run_directory.go ├── cmd_splay.go ├── cmd_ssl_expiry.go ├── cmd_timeout.go ├── cmd_todo.go ├── cmd_tree.go ├── cmd_urls.go ├── cmd_validate_json.go ├── cmd_validate_xml.go ├── cmd_validate_yaml.go ├── cmd_version.go ├── cmd_version_18.go ├── cmd_watch.go ├── cmd_with_lock.go ├── common.go ├── go.mod ├── go.sum ├── main.go └── templatedcmd ├── templatedcmd.go └── templatedcmd_test.go /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | github: skx 3 | custom: https://steve.fi/donate/ 4 | -------------------------------------------------------------------------------- /.github/build: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # The basename of our binary 4 | BASE="sysbox" 5 | 6 | # I don't even .. 7 | go env -w GOFLAGS="-buildvcs=false" 8 | 9 | # 10 | # We build on multiple platforms/archs 11 | # 12 | BUILD_PLATFORMS="linux darwin freebsd" 13 | BUILD_ARCHS="amd64 386" 14 | 15 | # For each platform 16 | for OS in ${BUILD_PLATFORMS[@]}; do 17 | 18 | # For each arch 19 | for ARCH in ${BUILD_ARCHS[@]}; do 20 | 21 | # Setup a suffix for the binary 22 | SUFFIX="${OS}" 23 | 24 | # i386 is better than 386 25 | if [ "$ARCH" = "386" ]; then 26 | SUFFIX="${SUFFIX}-i386" 27 | else 28 | SUFFIX="${SUFFIX}-${ARCH}" 29 | fi 30 | 31 | echo "Building for ${OS} [${ARCH}] -> ${BASE}-${SUFFIX}" 32 | 33 | # Run the build 34 | export GOARCH=${ARCH} 35 | export GOOS=${OS} 36 | export CGO_ENABLED=0 37 | 38 | # Build the main-binary 39 | go build -ldflags "-X main.versionString=$(git describe --tags 2>/dev/null || echo 'master')" -o "${BASE}-${SUFFIX}" 40 | done 41 | done 42 | -------------------------------------------------------------------------------- /.github/run-tests.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # I don't even .. 4 | go env -w GOFLAGS="-buildvcs=false" 5 | 6 | # Install the tools we use to test our code-quality. 7 | # 8 | # Here we setup the tools to install only if the "CI" environmental variable 9 | # is not empty. This is because locally I have them installed. 10 | # 11 | # NOTE: Github Actions always set CI=true 12 | # 13 | if [ ! -z "${CI}" ] ; then 14 | go install golang.org/x/lint/golint@latest 15 | go install golang.org/x/tools/go/analysis/passes/shadow/cmd/shadow@latest 16 | go install honnef.co/go/tools/cmd/staticcheck@latest 17 | fi 18 | 19 | # Run the static-check tool. 20 | t=$(mktemp) 21 | staticcheck -checks all ./... > $t 22 | if [ -s $t ]; then 23 | echo "Found errors via 'staticcheck'" 24 | cat $t 25 | rm $t 26 | exit 1 27 | fi 28 | rm $t 29 | 30 | # At this point failures cause aborts 31 | set -e 32 | 33 | # Run the linter 34 | echo "Launching linter .." 35 | golint -set_exit_status ./... 36 | echo "Completed linter .." 37 | 38 | # Run the shadow-checker 39 | echo "Launching shadowed-variable check .." 40 | go vet -vettool=$(which shadow) ./... 41 | echo "Completed shadowed-variable check .." 42 | 43 | # Run any test-scripts we have (i.e. calc/) 44 | go test ./... 45 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | name: "CodeQL" 7 | 8 | on: 9 | push: 10 | branches: [master] 11 | pull_request: 12 | # The branches below must be a subset of the branches above 13 | branches: [master] 14 | schedule: 15 | - cron: '0 6 * * 4' 16 | 17 | jobs: 18 | analyze: 19 | name: Analyze 20 | runs-on: ubuntu-latest 21 | 22 | strategy: 23 | fail-fast: false 24 | matrix: 25 | # Override automatic language detection by changing the below list 26 | # Supported options are ['csharp', 'cpp', 'go', 'java', 'javascript', 'python'] 27 | language: ['go'] 28 | # Learn more... 29 | # https://docs.github.com/en/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#overriding-automatic-language-detection 30 | 31 | steps: 32 | - name: Checkout repository 33 | uses: actions/checkout@v2 34 | with: 35 | # We must fetch at least the immediate parents so that if this is 36 | # a pull request then we can checkout the head. 37 | fetch-depth: 2 38 | 39 | # If this run was triggered by a pull request event, then checkout 40 | # the head of the pull request instead of the merge commit. 41 | - run: git checkout HEAD^2 42 | if: ${{ github.event_name == 'pull_request' }} 43 | 44 | # Initializes the CodeQL tools for scanning. 45 | - name: Initialize CodeQL 46 | uses: github/codeql-action/init@v1 47 | with: 48 | languages: ${{ matrix.language }} 49 | # If you wish to specify custom queries, you can do so here or in a config file. 50 | # By default, queries listed here will override any specified in a config file. 51 | # Prefix the list here with "+" to use these queries and those in the config file. 52 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 53 | 54 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 55 | # If this step fails, then you should remove it and run the build manually (see below) 56 | - name: Autobuild 57 | uses: github/codeql-action/autobuild@v1 58 | 59 | # ℹ️ Command-line programs to run using the OS shell. 60 | # 📚 https://git.io/JvXDl 61 | 62 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 63 | # and modify them (or add more) to build your code if your project 64 | # uses a compiled language 65 | 66 | #- run: | 67 | # make bootstrap 68 | # make release 69 | 70 | - name: Perform CodeQL Analysis 71 | uses: github/codeql-action/analyze@v1 72 | -------------------------------------------------------------------------------- /.github/workflows/pull_request.yml: -------------------------------------------------------------------------------- 1 | on: pull_request 2 | name: Pull Request 3 | jobs: 4 | test: 5 | name: Test 6 | runs-on: ubuntu-latest 7 | steps: 8 | - uses: actions/checkout@master 9 | - name: Test 10 | uses: skx/github-action-tester@master 11 | -------------------------------------------------------------------------------- /.github/workflows/push.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: 4 | - master 5 | name: Push Event 6 | jobs: 7 | test: 8 | name: Test 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@master 12 | - name: Test 13 | uses: skx/github-action-tester@master 14 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | on: release 2 | name: Handle Release 3 | jobs: 4 | upload: 5 | name: Upload 6 | runs-on: ubuntu-latest 7 | steps: 8 | - name: Checkout the repository 9 | uses: actions/checkout@master 10 | - name: Generate the artifacts 11 | uses: skx/github-action-build@master 12 | - name: Upload the artifacts 13 | uses: skx/github-action-publish-binaries@master 14 | env: 15 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 16 | with: 17 | args: sysbox-* 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | sysbox 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Go Report Card](https://goreportcard.com/badge/github.com/skx/sysbox)](https://goreportcard.com/report/github.com/skx/sysbox) 2 | [![license](https://img.shields.io/github/license/skx/sysbox.svg)](https://github.com/skx/sysbox/blob/master/LICENSE) 3 | [![Release](https://img.shields.io/github/release/skx/sysbox.svg)](https://github.com/skx/sysbox/releases/latest) 4 | 5 | 6 | * [SysBox](#sysbox) 7 | * [Installation](#installation) 8 | * [Bash Completion](#bash-completion) 9 | * [Overview](#overview) 10 | * [Tools](#tools) 11 | * [Future Additions?](#future-additions) 12 | * [Github Setup](#github-setup) 13 | 14 | 15 | 16 | 17 | # SysBox 18 | 19 | This repository is the spiritual successor to my previous [sysadmin-utils repository](https://github.com/skx/sysadmin-util) 20 | 21 | The idea here is to collect simple utilities and package them as a single binary, written in go, in a similar fashion to the `busybox` utility. 22 | 23 | 24 | 25 | ## Installation 26 | 27 | Installation upon a system which already contains a go-compiler should be as simple as: 28 | 29 | ``` 30 | $ go install github.com/skx/sysbox@latest 31 | ``` 32 | 33 | If you've cloned [this repository](https://github.com/skx/sysbox) then the following will suffice: 34 | 35 | ``` 36 | $ go build . 37 | $ go install . 38 | ``` 39 | 40 | Finally may find binary releases for various systems upon our [download page](https://github.com/skx/sysbox/releases). 41 | 42 | 43 | 44 | ## Bash Completion 45 | 46 | The [subcommand library](https://github.com/skx/subcommands) this application uses has integrated support for the generation of a completion script for the bash shell. 47 | 48 | To enable this add the following to your bash configuration-file: 49 | 50 | ``` 51 | source <(sysbox bash-completion) 52 | 53 | ``` 54 | 55 | 56 | 57 | 58 | # Overview 59 | 60 | This application is built, and distributed, as a single-binary named `sysbox`, which implements a number of sub-commands. 61 | 62 | You can either run the tools individually, taking advantage of the [bash completion](#bash-completion) support to complete the subcommands and their arguments: 63 | 64 | $ sysbox foo .. 65 | $ sysbox bar .. 66 | 67 | Or you can create symlinks to allow specific tool to be executed without the need to specify a subcommand: 68 | 69 | $ ln -s $(which sysbox) /usr/local/bin/calc 70 | $ /usr/local/bin/calc '3 * 3' 71 | 9 72 | 73 | 74 | 75 | 76 | # Tools 77 | 78 | The tools in this repository started out as being simple ports of the tools in my [previous repository](https://github.com/skx/sysadmin-util), however I've now started to expand them and fold in things I've used/created in the past. 79 | 80 | You can view a summary of the available subcommands via: 81 | 82 | $ sysbox help 83 | 84 | More complete help for each command should be available like so: 85 | 86 | $ sysbox help sub-command 87 | 88 | Examples are included where useful. 89 | 90 | 91 | 92 | ## calc 93 | 94 | A simple calculator, which understands floating point-operations, unlike `expr`, and has some simple line-editing facilities built into its REPL-mode. 95 | 96 | The calculator supports either execution of sums via via the command-line, or as an interactive REPL environment: 97 | 98 | ``` 99 | $ sysbox calc 3.1 + 2.7 100 | 5.8 101 | 102 | $ sysbox calc 103 | calc> let a = 1/3 104 | 0.333333 105 | calc> result * 9 106 | 3 107 | calc> result * 9 108 | 27 109 | calc> exit 110 | $ 111 | ``` 112 | 113 | Here you see the magic variable `result` is always updated to store the value of the previous calculation. 114 | 115 | 116 | 117 | ## choose-file 118 | 119 | This subcommand presents a console-based UI to select a file. The file selected will be displayed upon STDOUT. The list may be filtered via an input-field. 120 | 121 | Useful for launching videos, emulators, etc: 122 | 123 | * `sysbox choose-file -execute="xine -g --no-logo --no-splash -V=40 {}" ~/Videos` 124 | * Choose a file, and execute `xine` with that filename as one of the arguments. 125 | * `xine $(sysbox choose-file ~/Videos)` 126 | * Use the STDOUT result to launch instead. 127 | 128 | The first form is preferred, because if the selection is canceled nothing happens. In the second-case `xine` would be launched with no argument. 129 | 130 | 131 | 132 | ## choose-stdin 133 | 134 | Almost identical to `choose-file`, but instead of allowing the user to choose from a filename it allows choosing from the contents read on STDIN. For example you might allow choosing a directory: 135 | 136 | ``` 137 | $ find ~/Repos -type d | sysbox choose-stdin -execute="firefox {}" 138 | ``` 139 | 140 | 141 | 142 | ## chronic 143 | 144 | The chronic command is ideally suited to wrap cronjobs, it runs the command you specify as a child process and hides the output produced __unless__ that process exits with a non-zero exit-code. 145 | 146 | 147 | 148 | ## comments 149 | 150 | This is a simple utility which outputs the comments found in the files named upon the command-line. Supported comments include C-style single-line comments (prefixed with `//`), C++-style multi-line comments (between `/*` and `*/`), and shell-style comments prefixed with `#`. 151 | 152 | Used for submitting pull-requests to projects about typos - as discussed [here upon my blog](https://blog.steve.fi/i_m_a_bit_of_a_git__hacker__.html). 153 | 154 | 155 | 156 | ## collapse 157 | 158 | This is a simple tool which will read STDIN, and output the content without any extra white-space: 159 | 160 | * Leading/Trailing white-space will be removed from every line. 161 | * Empty lines will be skipped entirely. 162 | 163 | 164 | 165 | ## cpp 166 | 167 | Something _like_ the C preprocessor, but supporting only the ability to include files, and run commands via `#include` and `#execute` respectively: 168 | 169 | #include "file/goes/here" 170 | #execute ls -l | wc -l 171 | 172 | See also `env-template` which allows more flexibility in running commands, and including files (or parts of files) via templates. 173 | 174 | 175 | 176 | ## env-template 177 | 178 | Perform expansion of golang `text/template` files, with support for getting environmental variables, running commands, and reading other files. 179 | 180 | You can freely use any of the available golang template facilities, for example please see the sample template here [cmd_env_template.tmpl](cmd_env_template.tmpl), and the the examples included in the [text/template documentation](https://golang.org/pkg/text/template/). 181 | 182 | > As an alternative you can consider the `envsubst` binary contained in your system's `gettext{-base}` package. 183 | 184 | **NOTE**: This sub-command also allows file-inclusion, in three different ways: 185 | 186 | * Including files literally. 187 | * Including lines from a file which match a particular regular expression. 188 | * Including the region from a file which is bounded by two regular expressions. 189 | 190 | See `sysbox help env-template` for further details, and examples. You'll also 191 | see it is possible to execute arbitrary commands and read their output. This facility was inspired by the [embedmd](https://github.com/campoy/embedmd) utility, and added in [#17](https://github.com/skx/sysbox/issues/17). 192 | 193 | See also `cpp` for a less flexible alternative which is useful for mere file inclusion and command-execution. 194 | 195 | 196 | 197 | ## exec-stdin 198 | 199 | Read STDIN, and allow running a command for each line. You can refer to 200 | the line read either completely, or by fields. 201 | 202 | For example: 203 | 204 | ``` 205 | $ ps -ef | sysbox exec-stdin echo field1:{1} field2:{2} line:{} 206 | ``` 207 | 208 | See the usage-information for more details (`sysbox help exec-stdin`), but consider this a simple union of `awk`, `xargs`, and GNU parallel (since we can run multiple commands in parallel). 209 | 210 | 211 | 212 | ## expect 213 | 214 | expect allows you to spawn a process, and send input in response to given output read from that process. It can be used to perform simple scripting operations against remote routers, etc. 215 | 216 | For examples please consult the output of `sysbox help expect`, but a simple example would be the following, which uses telnet to connect to a remote host and run a couple of commands. Note that we use `\r\n` explicitly, due to telnet being in use, and that there is no password-authentication required in this example: 217 | 218 | ```sh 219 | $ cat script.in 220 | SPAWN telnet telehack.com 221 | EXPECT \n\. 222 | SEND date\r\n 223 | EXPECT \n\. 224 | SEND quit\r\n 225 | 226 | $ sysbox expect script.in 227 | ``` 228 | 229 | 230 | 231 | ## feeds 232 | 233 | The feeds sub-command retrieves the contents of the (single) remote URL which is specified, and outputs a list of all the RSS/Atom feeds which have been referenced within that file. 234 | 235 | Basic usage would be: 236 | 237 | $ sysbox feeds https://blog.steve.fi/ 238 | 239 | If no protocol is specified "https" is assumed, (for example an argument of "example.com" will be converted to https://example.com). 240 | 241 | 242 | 243 | ## find 244 | 245 | The find sub-command allows finding files/directories that match a given number 246 | of regular expressions. Basic usage is: 247 | 248 | $ sysbox find foo bar$ 249 | 250 | By default the names of files are shown, but you can view either files, directories, or both. The starting point will be the current working directory, but `-path` can be used to change that: 251 | 252 | $ sysbox find -path /etc -files=true -directories=true '(i?)magic' 253 | /etc/ImageMagick-6/magic.xml 254 | /etc/magic 255 | /etc/magic.mime 256 | /etc/sane.d/magicolor.conf 257 | 258 | 259 | 260 | ## fingerd 261 | 262 | A trivial finger-server. 263 | 264 | 265 | 266 | ## html2text 267 | 268 | A simple tool for converting from HTML to Text, added when I realized I'd 269 | connected to a system over `ssh` and there were no console viewers installed, 270 | (such as `lynx`, `links`, or `w3m`). 271 | 272 | 273 | 274 | ## httpd 275 | 276 | A simple HTTP-server. Allows serving to localhost, or to the local LAN. 277 | 278 | 279 | 280 | ## http-get 281 | 282 | Very much "curl-lite", allows you to fetch the contents of a remote URL. SSL errors, etc, are handled, but only minimal options are supported. 283 | 284 | Basic usage would be: 285 | 286 | $ sysbox http-get https://example.com/ 287 | 288 | If no protocol is specified "https" is assumed, (for example an argument of "example.com" will be converted to https://example.com). 289 | 290 | 291 | 292 | ## ips 293 | 294 | This tool lets you easily retrieve a list of local, or global, IPv4 and 295 | IPv6 addresses present upon your local host. This is a little simpler 296 | than trying to parse `ip -4 addr list`, although that is also the 297 | common approach. 298 | 299 | 300 | 301 | ## markdown-toc 302 | 303 | This tool creates a simple Markdown table-of-contents, which is useful 304 | for the `README.md` files as used on github. 305 | 306 | 307 | 308 | ## make-password 309 | 310 | This tool generates a single random password each time it is executed, it is designed to be quick and simple to use, rather than endlessly configurable. 311 | 312 | 313 | 314 | ## rss 315 | 316 | Show a summary of the contents of the given RSS feed. By default the links to the individual entries are shown, but it is possible to use a format-string to show more. 317 | 318 | 319 | 320 | ## run-directory 321 | 322 | Run every executable in the given directory, optionally terminate if any command returns a non-zero exit-code. 323 | 324 | > The exit-code handling is what inspired this addition; the Debian version of `run-parts` supports this, but the CentOS version does not. 325 | 326 | 327 | 328 | ## splay 329 | 330 | This tool allows sleeping for a random amount of time. This solves the problem when you have a hundred servers all running a task at the same time, triggered by `cron`, and you don't want to overwhelm a central resource that they each consume. 331 | 332 | 333 | 334 | ## ssl-expiry 335 | 336 | A simple utility to report upon the number of hours, and days, until a given TLS certificate (or any intermediary in the chain) expires. 337 | 338 | Ideal for https-servers, but also TLS-protected SMTP hosts, etc. 339 | 340 | 341 | 342 | ## timeout 343 | 344 | Run a command, but kill it after the given number of seconds. The command is executed with a PTY so you can run interactive things such as `top`, `mutt`, etc. 345 | 346 | 347 | 348 | ## todo 349 | 350 | A command to look for TODO items which contain dates in the past, the idea 351 | being that you can record notes for yourself, along with deadlines, in your 352 | code. Later you can see which deadlines have been exceeded. 353 | 354 | 355 | 356 | ## tree 357 | 358 | Trivial command to display the contents of a filesystem, as a nested tree. This is similar to the standard `tree` command, without the nesting and ASCII graphics. 359 | 360 | 361 | 362 | ## urls 363 | 364 | Extract URLs from the named files, or STDIN. URLs are parsed naively with a simple regular expression and only `http` and `https` schemes are recognized. 365 | 366 | 367 | 368 | ## validate-json 369 | 370 | Validate JSON files for correctness and syntax-errors. 371 | 372 | 373 | 374 | ## validate-xml 375 | 376 | Validate XML files for correctness and syntax-errors. 377 | 378 | 379 | 380 | ## version 381 | 382 | Report the version of the binary, when downloaded from our [release page](https://github.com/skx/sysbox/releases). 383 | 384 | 385 | 386 | ## validate-yaml 387 | 388 | Validate YAML files for correctness and syntax-errors. 389 | 390 | 391 | 392 | ## watch 393 | 394 | Execute the same command constantly, with a small delay. Useful to observe a command-completing. 395 | 396 | 397 | 398 | ## with-lock 399 | 400 | Allow running a command with a lock-file to prevent parallel executions. 401 | 402 | This is perfect if you fear your cron-jobs will start slowing down and overlapping executions will cause problems. 403 | 404 | 405 | 406 | 407 | # Future Additions? 408 | 409 | Unlike the previous repository I'm much happier to allow submissions of new utilities, or sub-commands, in this repository. 410 | 411 | 412 | 413 | 414 | # Github Setup 415 | 416 | This repository is configured to run tests upon every commit, and when 417 | pull-requests are created/updated. The testing is carried out via 418 | [.github/run-tests.sh](.github/run-tests.sh) which is used by the 419 | [github-action-tester](https://github.com/skx/github-action-tester) action. 420 | 421 | Releases are automated in a similar fashion via [.github/build](.github/build), 422 | and the [github-action-publish-binaries](https://github.com/skx/github-action-publish-binaries) action. 423 | 424 | Steve 425 | -- 426 | -------------------------------------------------------------------------------- /calc/FUZZING.md: -------------------------------------------------------------------------------- 1 | # Fuzz-Testing 2 | 3 | The 1.18 release of the golang compiler/toolset has integrated support for 4 | fuzz-testing. 5 | 6 | Fuzz-testing is basically magical and involves generating new inputs "randomly" 7 | and running test-cases with those inputs. 8 | 9 | 10 | ## Running 11 | 12 | If you're running 1.18beta1 or higher you can run the fuzz-testing against 13 | the calculator package like so: 14 | 15 | $ go test -fuzztime=300s -parallel=1 -fuzz=FuzzCalculator -v 16 | === RUN TestBasic 17 | --- PASS: TestBasic (0.00s) 18 | .. 19 | .. 20 | fuzz: elapsed: 0s, gathering baseline coverage: 0/111 completed 21 | fuzz: elapsed: 0s, gathering baseline coverage: 111/111 completed, now fuzzing with 1 workers 22 | fuzz: elapsed: 3s, execs: 63894 (21292/sec), new interesting: 9 (total: 120) 23 | fuzz: elapsed: 6s, execs: 76044 (4051/sec), new interesting: 12 (total: 123) 24 | fuzz: elapsed: 9s, execs: 76044 (0/sec), new interesting: 12 (total: 123) 25 | fuzz: elapsed: 12s, execs: 76044 (0/sec), new interesting: 12 (total: 123) 26 | .. 27 | fuzz: elapsed: 5m0s, execs: 5209274 (12462/sec), new interesting: 162 (total: 273) 28 | fuzz: elapsed: 5m1s, execs: 5209274 (0/sec), new interesting: 162 (total: 273) 29 | --- PASS: FuzzCalculator (301.01s) 30 | PASS 31 | ok github.com/skx/sysbox/calc 301.010s 32 | 33 | You'll note that I've added `-parallel=1` to the test, because otherwise my desktop system becomes unresponsive while the testing is going on. 34 | -------------------------------------------------------------------------------- /calc/doc.go: -------------------------------------------------------------------------------- 1 | // Package calc contains a simple calculator, which supports the 2 | // most basic operations: 3 | // 4 | // + 5 | // - 6 | // * 7 | // / 8 | // 9 | // In addition to the basic operations it is also possible to 10 | // declare variables via `let name = xxx`, for example: 11 | // 12 | // let a = 3 -> 3 13 | // a / 9 -> 0.3333 14 | // 15 | // The two variables `pi` and `e` are available by default. 16 | package calc 17 | -------------------------------------------------------------------------------- /calc/evaluator.go: -------------------------------------------------------------------------------- 1 | package calc 2 | 3 | import ( 4 | "fmt" 5 | "math" 6 | ) 7 | 8 | // Evaluator holds the state of the evaluation-object. 9 | type Evaluator struct { 10 | 11 | // tokens holds the series of tokens which our 12 | // lexer produced from our input. 13 | tokens []*Token 14 | 15 | // Current position within the array of tokens. 16 | token int 17 | 18 | // holder for any variables the user has defined. 19 | variables map[string]float64 20 | } 21 | 22 | // New creates a new evaluation object. 23 | // 24 | // The evaluation object starts out as being empty, 25 | // but you can call Load to load an expression and 26 | // then Run to execute it. 27 | func New() *Evaluator { 28 | 29 | // Create the new object. 30 | e := &Evaluator{} 31 | 32 | // Populate the variable storage-store. 33 | e.variables = make(map[string]float64) 34 | 35 | // Load default constants. 36 | e.variables["pi"] = math.Pi 37 | e.variables["e"] = math.E 38 | 39 | return e 40 | } 41 | 42 | // Variable allows you to return the value of the given variable 43 | func (e *Evaluator) Variable(name string) (float64, bool) { 44 | res, ok := e.variables[name] 45 | return res, ok 46 | } 47 | 48 | // Load is used to load a program into the evaluator. 49 | // 50 | // Note that the existing variables will maintain their state 51 | // if not reset explicitly. 52 | func (e *Evaluator) Load(input string) { 53 | 54 | // Create a lexer for splitting the program 55 | lexer := NewLexer(input) 56 | 57 | // Remove any existing tokens 58 | e.tokens = nil 59 | 60 | // Parse the input into tokens, and 61 | // save them away. 62 | for { 63 | tok := lexer.Next() 64 | if tok.Type == EOF { 65 | break 66 | } 67 | e.tokens = append(e.tokens, tok) 68 | } 69 | 70 | // Add an extra pair of EOF tokens so that nextToken 71 | // can always be called 72 | e.tokens = append(e.tokens, &Token{Value: "EOF", Type: EOF}) 73 | e.tokens = append(e.tokens, &Token{Value: "EOF", Type: EOF}) 74 | e.token = 0 75 | } 76 | 77 | // nextToken returns the next token from our input. 78 | // 79 | // When Load is called the input-expression is broken 80 | // down into a series of Tokens, and this function 81 | // advances to the next token, returning it. 82 | func (e *Evaluator) nextToken() *Token { 83 | tok := e.tokens[e.token] 84 | e.token++ 85 | return tok 86 | } 87 | 88 | // peekToken returns the next pending token in our stream. 89 | // 90 | // NOTE it is always possible to peek at the next token, 91 | // because we deliberately add an extra/spare EOF token 92 | // in our constructor. 93 | func (e *Evaluator) peekToken() *Token { 94 | tok := e.tokens[e.token] 95 | return tok 96 | } 97 | 98 | // term() - return a term 99 | func (e *Evaluator) term() *Token { 100 | 101 | f1 := e.factor() 102 | 103 | // error handling 104 | if f1.Type == ERROR { 105 | return f1 106 | } 107 | 108 | op := e.peekToken() 109 | for op.Type == MULTIPLY || op.Type == DIVIDE { 110 | 111 | op = e.nextToken() 112 | 113 | f2 := e.factor() 114 | 115 | if f1.Type != NUMBER { 116 | return &Token{Type: ERROR, Value: fmt.Sprintf("%v is not a number", f1)} 117 | } 118 | if f2.Type != NUMBER { 119 | return &Token{Type: ERROR, Value: fmt.Sprintf("%v is not a number", f2)} 120 | } 121 | 122 | if op.Type == MULTIPLY { 123 | f1.Value = f1.Value.(float64) * f2.Value.(float64) 124 | } 125 | if op.Type == DIVIDE { 126 | if f2.Value.(float64) == 0 { 127 | f1 = &Token{Type: ERROR, Value: fmt.Sprintf("Attempted division by zero: %v/%v", f1, f2)} 128 | } else { 129 | f1.Value = f1.Value.(float64) / f2.Value.(float64) 130 | } 131 | } 132 | 133 | op = e.peekToken() 134 | } 135 | 136 | if op.Type == ERROR { 137 | return &Token{Type: ERROR, Value: fmt.Sprintf("Unexpected token inside term() - %v\n", op)} 138 | } 139 | 140 | return f1 141 | } 142 | 143 | // expr() parse an expression 144 | func (e *Evaluator) expr() *Token { 145 | 146 | t1 := e.term() 147 | 148 | // 149 | // If we have an assignment we'll save the result 150 | // of the expression here. 151 | // 152 | // We do this to avoid repetition for "let x = ..." and 153 | // "y = ..." 154 | // 155 | variable := &Token{Type: ERROR, Value: "cant happen"} 156 | 157 | // 158 | // Assignment without LET ? 159 | // 160 | if t1.Type == IDENT { 161 | nxt := e.peekToken() 162 | if nxt.Type == ASSIGN { 163 | 164 | // Skip the assignment 165 | e.nextToken() 166 | 167 | // And we've found a variable to assign to 168 | variable = t1 169 | } 170 | } 171 | 172 | // 173 | // Assignment with LET 174 | // 175 | if t1.Type == LET { 176 | 177 | // Get the identifier. 178 | ident := e.nextToken() 179 | if ident.Type != IDENT { 180 | return &Token{Type: ERROR, Value: fmt.Sprintf("%v is not an identifier", ident)} 181 | } 182 | 183 | // Skip the assignment statement 184 | assign := e.nextToken() 185 | if assign.Type != ASSIGN { 186 | return &Token{Type: ERROR, Value: fmt.Sprintf("%v is not an assignment statement", ident)} 187 | } 188 | 189 | variable = ident 190 | } 191 | 192 | // 193 | // OK if we have an assignment, of either form, then 194 | // process it here. 195 | // 196 | if variable.Type == IDENT { 197 | 198 | // Calculate the result 199 | result := e.expr() 200 | 201 | // Save it, and also return the value. 202 | if result.Type == NUMBER { 203 | e.variables[variable.Value.(string)] = result.Value.(float64) 204 | } 205 | return result 206 | } 207 | 208 | // 209 | // If we reach here we're now done with assignments. 210 | // 211 | tok := e.peekToken() 212 | for tok.Type == PLUS || tok.Type == MINUS { 213 | 214 | tok = e.nextToken() 215 | t2 := e.term() 216 | 217 | if t1.Type != NUMBER { 218 | return &Token{Type: ERROR, Value: fmt.Sprintf("%v is not a number", t1)} 219 | } 220 | if t2.Type != NUMBER { 221 | return &Token{Type: ERROR, Value: fmt.Sprintf("%v is not a number", t2)} 222 | } 223 | 224 | if tok.Type == PLUS { 225 | t1.Value = t1.Value.(float64) + t2.Value.(float64) 226 | } 227 | if tok.Type == MINUS { 228 | t1.Value = t1.Value.(float64) - t2.Value.(float64) 229 | } 230 | 231 | tok = e.peekToken() 232 | } 233 | 234 | if tok.Type == ERROR { 235 | return &Token{Type: ERROR, Value: fmt.Sprintf("Unexpected token inside expr() - %v\n", tok)} 236 | } 237 | 238 | return t1 239 | } 240 | 241 | // factor() - return a token 242 | func (e *Evaluator) factor() *Token { 243 | tok := e.nextToken() 244 | 245 | switch tok.Type { 246 | case EOF: 247 | return &Token{Type: ERROR, Value: "unexpected EOF in factor()"} 248 | case NUMBER: 249 | return tok 250 | case IDENT: 251 | 252 | // sleazy hack here. 253 | // 254 | // We're getting a factor, but if we have a variable 255 | // AND the next token is an assignment then we return 256 | // the ident (i.e. current token) to allow Run() to 257 | // process "var = expr" 258 | // 259 | // Without this we'd interpret "foo = 1 + 2" as 260 | // a reference to the preexisting variable "foo" 261 | // which would not exist. 262 | // 263 | nxt := e.peekToken() 264 | if nxt.Type == ASSIGN { 265 | return tok 266 | } 267 | 268 | // 269 | // OK lookup the content of an existing variable. 270 | // 271 | val, ok := e.variables[tok.Value.(string)] 272 | if ok { 273 | return &Token{Value: val, Type: NUMBER} 274 | } 275 | return &Token{Type: ERROR, Value: fmt.Sprintf("undefined variable: %s", tok.Value.(string))} 276 | case LET: 277 | return tok 278 | case LPAREN: 279 | // 280 | // We don't need to skip past the `(` here 281 | // because the `expr` call will do that 282 | // to find its arguments 283 | // 284 | 285 | // evaluate the expression 286 | res := e.expr() 287 | 288 | // next token should be ")" 289 | if e.peekToken().Type != RPAREN { 290 | return &Token{Type: ERROR, Value: fmt.Sprintf("expected ')' after expression found %v", e.peekToken())} 291 | } 292 | 293 | // skip that ")" 294 | e.nextToken() 295 | 296 | return res 297 | case MINUS: 298 | // If the next token is a number then we're good 299 | if e.peekToken().Type == NUMBER { 300 | val := e.nextToken() 301 | cur := val.Value.(float64) 302 | val.Value = cur * -1 303 | return val 304 | } 305 | } 306 | 307 | return &Token{Type: ERROR, Value: fmt.Sprintf("Unexpected token inside factor() - %v\n", tok)} 308 | } 309 | 310 | // Run launches the program we've loaded. 311 | // 312 | // If multiple statements are available each are executed in turn, 313 | // and the result of the last one returned. However errors will 314 | // cause early-termination. 315 | func (e *Evaluator) Run() *Token { 316 | 317 | var result *Token 318 | 319 | // Process each statement 320 | for e.peekToken().Type != EOF && e.peekToken().Type != ERROR { 321 | 322 | // Get the result 323 | result = e.expr() 324 | 325 | // Error? Then abort 326 | if result.Type == ERROR { 327 | return result 328 | } 329 | 330 | // Otherwise loop again. 331 | } 332 | 333 | // Did we terminate on an error? 334 | if e.peekToken().Type == ERROR { 335 | return e.peekToken() 336 | } 337 | 338 | // If we evaluated something we'll have a result which 339 | // we'll save in the `result` variable. 340 | // 341 | // (We might receive input such as "", which will result 342 | // in nothing being evaluated) 343 | if result != nil { 344 | e.variables["result"] = result.Value.(float64) 345 | } 346 | 347 | // All done. 348 | return result 349 | } 350 | -------------------------------------------------------------------------------- /calc/evaluator_test.go: -------------------------------------------------------------------------------- 1 | package calc 2 | 3 | import ( 4 | "math" 5 | "strings" 6 | "testing" 7 | ) 8 | 9 | const float64EqualityThreshold = 1e-5 10 | 11 | // Floating points are hard. 12 | func almostEqual(a, b float64) bool { 13 | return math.Abs(a-b) <= float64EqualityThreshold 14 | } 15 | 16 | // Test some basic operations 17 | func TestBasic(t *testing.T) { 18 | 19 | tests := []struct { 20 | input string 21 | output float64 22 | }{ 23 | {"1", 1}, 24 | {"1 + 2", 3}, 25 | {"1 + 2 * 3", 7}, 26 | {"1 / 3", 1.0 / 3}, 27 | {"1 / 3 * 9", 3}, 28 | {"( 1 / 3 ) * 9", 3}, 29 | {"1 - 3", -2}, 30 | {"3--3", 6}, // 3 - (-3) 31 | {"-1 + 3", 2}, 32 | {"( 1 + 2 ) * 4", 12}, 33 | {"( ( 1 + 2 ) * 4 )", 12}, 34 | } 35 | 36 | for _, test := range tests { 37 | 38 | p := New() 39 | p.Load(test.input) 40 | 41 | out := p.Run() 42 | 43 | if out.Type != NUMBER { 44 | t.Fatalf("Output was not a number: %v\n", out) 45 | } 46 | if !almostEqual(out.Value.(float64), test.output) { 47 | t.Fatalf("Got wrong result for '%s', expected '%f' found '%f'", test.input, test.output, out.Value.(float64)) 48 | } 49 | } 50 | } 51 | 52 | // Test for errors 53 | func TestDivideZero(t *testing.T) { 54 | 55 | tests := []struct { 56 | input string 57 | }{ 58 | {"1 / 0"}, 59 | {"let a = 1 ; let b = 0 ; a / b ;"}, 60 | } 61 | 62 | for _, test := range tests { 63 | 64 | p := New() 65 | p.Load(test.input) 66 | 67 | out := p.Run() 68 | 69 | if out.Type != ERROR { 70 | t.Fatalf("expected error, found none") 71 | } 72 | if !strings.Contains(out.Value.(string), "division by zero") { 73 | t.Fatalf("division by zero error expected, but found %s", out.Value.(string)) 74 | } 75 | } 76 | } 77 | 78 | // Test for errors 79 | func TestMissingVariable(t *testing.T) { 80 | 81 | tests := []struct { 82 | input string 83 | }{ 84 | {"let a = 1 + b"}, 85 | {"let a = 1 - b"}, 86 | {"let a = 1 / b"}, 87 | {"let a = 1 * b"}, 88 | 89 | {"let a = b + 1"}, 90 | {"let a = b - 1"}, 91 | {"let a = b / 1"}, 92 | {"let a = b * 2"}, 93 | } 94 | 95 | for _, test := range tests { 96 | 97 | p := New() 98 | p.Load(test.input) 99 | 100 | out := p.Run() 101 | 102 | if out.Type != ERROR { 103 | t.Fatalf("expected error, found none") 104 | } 105 | if !strings.Contains(out.Value.(string), "undefined variable") { 106 | t.Fatalf("undefined variable error expected, but found %s", out.Value.(string)) 107 | } 108 | } 109 | } 110 | 111 | // TestErrorCases looks for some basic errors. 112 | func TestErrorCases(t *testing.T) { 113 | 114 | tests := []struct { 115 | input string 116 | error string 117 | }{ 118 | {"let 1 = 1", "is not an identifier"}, 119 | {"let a = 1 / let", "is not a number"}, 120 | {"let a = let / 3 ", "is not a number"}, 121 | {"let 1 = 1", "is not an identifier"}, 122 | {"let foo = ; ", "EOF"}, 123 | {"let foo foo ; ", "not an assignment statement"}, 124 | {"let foo = ( 1 + 2 * 3 ", "expected ')'"}, 125 | {")", "Unexpected token inside factor"}, 126 | {"3.3.3", "too many periods"}, 127 | {"3 / 3 + $", "Unexpected token inside factor"}, 128 | {"3 + 3 $", "Unexpected token inside term"}, 129 | 130 | // eof 131 | {"3 + ", "unexpected EOF in factor"}, 132 | {"3 + 3 / ", "unexpected EOF in factor"}, 133 | {"3 + 3 * ", "unexpected EOF in factor"}, 134 | 135 | // `let` is a LET token, not a generic identifier. 136 | {"let let = 3", " is not an identifier"}, 137 | } 138 | 139 | for _, test := range tests { 140 | 141 | p := New() 142 | p.Load(test.input) 143 | 144 | out := p.Run() 145 | 146 | if out.Type != ERROR { 147 | t.Fatalf("expected error, found none for input '%s'", test.input) 148 | } 149 | if !strings.Contains(out.Value.(string), test.error) { 150 | t.Fatalf("expected error '%s', but found %s", test.error, out.Value.(string)) 151 | } 152 | } 153 | } 154 | 155 | // TestAssign tests that assignment work. 156 | func TestAssign(t *testing.T) { 157 | tests := []struct { 158 | input string 159 | variable string 160 | value float64 161 | }{ 162 | // with let 163 | {"let a = 3", "a", 3}, 164 | {"let a = 1; let b = 2; let c = 3; let d = a+ b * c", "d", 7}, 165 | 166 | // without let 167 | {"a = 6", "a", 6}, 168 | {"a = 1; b = 2; c = 3; d = a + b * c", "d", 7}, 169 | } 170 | 171 | for _, test := range tests { 172 | 173 | p := New() 174 | p.Load(test.input) 175 | 176 | out := p.Run() 177 | 178 | if out.Type == ERROR { 179 | t.Fatalf("unexpected error '%s': %s", test.input, out.Value.(string)) 180 | } 181 | 182 | // get the variable 183 | result, found := p.Variable(test.variable) 184 | if !found { 185 | t.Fatalf("failed to lookup variable %s in %s", test.variable, test.input) 186 | } 187 | 188 | if result != test.value { 189 | t.Fatalf("result of '%s' should have been %f, got %f", test.input, test.value, result) 190 | } 191 | } 192 | } 193 | -------------------------------------------------------------------------------- /calc/fuzz_test.go: -------------------------------------------------------------------------------- 1 | //go:build go1.18 2 | // +build go1.18 3 | 4 | package calc 5 | 6 | import ( 7 | "testing" 8 | ) 9 | 10 | // FuzzCalculator will run a series of random/fuzz-tests against our 11 | // parser and evaluator. 12 | func FuzzCalculator(f *testing.F) { 13 | 14 | // Seed some "interesting" inputs 15 | f.Add([]byte("")) 16 | f.Add([]byte("\r")) 17 | f.Add([]byte("\n")) 18 | f.Add([]byte("\t")) 19 | f.Add([]byte("\r \n \t")) 20 | f.Add([]byte("3 / 3\n")) 21 | f.Add([]byte("3 - -3\r\n")) 22 | f.Add([]byte("3 / 0")) 23 | f.Add([]byte(nil)) 24 | 25 | // Run the fuzzer 26 | f.Fuzz(func(t *testing.T, input []byte) { 27 | 28 | // Create 29 | cal := New() 30 | 31 | // Parser 32 | cal.Load(string(input)) 33 | 34 | // Evaluate 35 | cal.Run() 36 | }) 37 | } 38 | -------------------------------------------------------------------------------- /calc/lexer.go: -------------------------------------------------------------------------------- 1 | // lexer.go - Contains our simple lexer, which returns tokens from 2 | // our input. 3 | // 4 | // These are NOT parsed into an AST, instead they are directly exected 5 | // by the evaluator. 6 | 7 | package calc 8 | 9 | import ( 10 | "fmt" 11 | "strconv" 12 | "strings" 13 | "unicode" 14 | ) 15 | 16 | // These constants are used to describe the type of token which has been lexed. 17 | const ( 18 | // Basic token-types 19 | EOF = "EOF" 20 | IDENT = "IDENT" 21 | NUMBER = "NUMBER" 22 | ERROR = "ERROR" 23 | 24 | // Assignment-magic 25 | LET = "LET" 26 | ASSIGN = "=" 27 | 28 | // Paren 29 | LPAREN = "(" 30 | RPAREN = ")" 31 | 32 | // Operations 33 | PLUS = "+" 34 | MINUS = "-" 35 | MULTIPLY = "*" 36 | DIVIDE = "/" 37 | ) 38 | 39 | // Token holds a lexed token from our input. 40 | type Token struct { 41 | 42 | // The type of the token. 43 | Type string 44 | 45 | // The value of the token. 46 | // 47 | // If the type of the token is NUMBER then this 48 | // will be stored as a float64. Otherwise the 49 | // value will be a string representation of the token. 50 | // 51 | Value interface{} 52 | } 53 | 54 | // Lexer holds our lexer state. 55 | type Lexer struct { 56 | 57 | // input is the string we're lexing. 58 | input string 59 | 60 | // position is the current position within the input-string. 61 | position int 62 | 63 | // simple map of single-character tokens to their type 64 | known map[string]string 65 | } 66 | 67 | // NewLexer creates a new lexer, for the given input. 68 | func NewLexer(input string) *Lexer { 69 | 70 | // Create the lexer object. 71 | l := &Lexer{input: input} 72 | 73 | // Populate the simple token-types in a map for later use. 74 | l.known = make(map[string]string) 75 | l.known["*"] = MULTIPLY 76 | l.known["+"] = PLUS 77 | l.known["-"] = MINUS 78 | l.known["/"] = DIVIDE 79 | l.known["="] = ASSIGN 80 | l.known["("] = LPAREN 81 | l.known[")"] = RPAREN 82 | 83 | return l 84 | } 85 | 86 | // Next returns the next token from our input stream. 87 | // 88 | // This is pretty naive lexer, however it is sufficient to 89 | // recognize numbers, identifiers, and our small set of 90 | // operators. 91 | func (l *Lexer) Next() *Token { 92 | 93 | // Loop until we've exhausted our input. 94 | for l.position < len(l.input) { 95 | 96 | // Get the next character 97 | char := string(l.input[l.position]) 98 | 99 | // Is this a known character/token? 100 | t, ok := l.known[char] 101 | if ok { 102 | // skip the character, and return the token 103 | l.position++ 104 | return &Token{Value: char, Type: t} 105 | } 106 | 107 | // If we reach here it is something more complex. 108 | switch char { 109 | 110 | // Skip whitespace 111 | case " ", "\n", "\r", "\t", ";": 112 | l.position++ 113 | continue 114 | 115 | // Is it a potential number? 116 | case "-", "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", ".": 117 | 118 | // 119 | // Loop for more digits 120 | // 121 | 122 | // Starting offset of our number 123 | start := l.position 124 | 125 | // ending offset of our number. 126 | end := l.position 127 | 128 | // keep walking forward, minding we don't wander 129 | // out of our input. 130 | for end < len(l.input) { 131 | 132 | if !l.isNumberComponent(l.input[end], end == start) { 133 | break 134 | } 135 | end++ 136 | } 137 | 138 | l.position = end 139 | 140 | // Here we have the number 141 | token := l.input[start:end] 142 | 143 | // too many periods? 144 | bits := strings.Split(token, ".") 145 | if len(bits) > 2 { 146 | return &Token{Type: ERROR, Value: fmt.Sprintf("too many periods in '%s'", token)} 147 | } 148 | 149 | // Convert to float64 150 | number, err := strconv.ParseFloat(token, 64) 151 | if err != nil { 152 | return &Token{Value: fmt.Sprintf("failed to parse number: %s", err.Error()), Type: ERROR} 153 | } 154 | 155 | return &Token{Value: number, Type: NUMBER} 156 | } 157 | 158 | // 159 | // We'll assume we have an identifier at this point. 160 | // 161 | 162 | // Starting offset of our ident 163 | start := l.position 164 | 165 | // ending offset of our ident. 166 | end := l.position 167 | 168 | // keep walking forward, minding we don't wander 169 | // out of our input. 170 | for end < len(l.input) { 171 | 172 | // Build up identifiers from any permitted 173 | // character. 174 | // 175 | // We allow unicode "letters" only. 176 | if l.isIdentifierCharacter(l.input[end]) { 177 | end++ 178 | } else { 179 | break 180 | } 181 | } 182 | 183 | // Change the position to be after the end of the identifier 184 | // we found - if we didn't find one then that results in no 185 | // change. 186 | l.position = end 187 | 188 | // Now record the text of the token (i.e. identifier). 189 | token := l.input[start:end] 190 | 191 | // 192 | // In a real language/lexer we might have 193 | // keywords/reserved-words to handle. 194 | // 195 | // We only need to cope with "let". 196 | // 197 | // If the identifier was LET then return that 198 | // token instead. 199 | // 200 | if strings.ToLower(token) == "let" { 201 | return &Token{Value: "let", Type: LET} 202 | } 203 | 204 | // 205 | // So we handled the easy cases, and then defaulted 206 | // to looking for our only supported identifier. 207 | // 208 | // If we failed to find one that means that we've got 209 | // to skip the unknown character - to avoid an infinite 210 | // loop. 211 | // 212 | // We'll skip over the character, and return the error. 213 | // 214 | if token == "" { 215 | l.position++ 216 | return &Token{Value: fmt.Sprintf("unknown character %c", l.input[end]), Type: ERROR} 217 | } 218 | 219 | // 220 | // We found a non-empty identifier, which 221 | // wasn't converted into a `let` keyword. 222 | // 223 | // Return it. 224 | // 225 | return &Token{Value: token, Type: IDENT} 226 | 227 | } 228 | 229 | // 230 | // If we get here then we've walked past the end of 231 | // our input-string. 232 | // 233 | return &Token{Value: "", Type: EOF} 234 | } 235 | 236 | // isIdentifierCharacter tests whether the given character is 237 | // valid for use in an identifier. 238 | func (l *Lexer) isIdentifierCharacter(d byte) bool { 239 | 240 | return (unicode.IsLetter(rune(d))) 241 | } 242 | 243 | // isNumberComponent looks for characters that can make up integers/floats 244 | // 245 | // We handle the first-character specially, which is why that's an argument 246 | func (l *Lexer) isNumberComponent(d byte, first bool) bool { 247 | 248 | // digits 249 | if unicode.IsDigit(rune(d)) { 250 | return true 251 | } 252 | 253 | // floating-point numbers require the use of "." 254 | if d == '.' { 255 | return true 256 | } 257 | 258 | // negative sign can only occur at the start of the input 259 | if d == '-' && first { 260 | return true 261 | } 262 | 263 | // No, this is not part of a number. 264 | return false 265 | } 266 | -------------------------------------------------------------------------------- /calc/lexer_test.go: -------------------------------------------------------------------------------- 1 | package calc 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | "testing" 7 | ) 8 | 9 | // Test basic invocation of our lexer. 10 | func TestLexer(t *testing.T) { 11 | 12 | tests := []struct { 13 | expectedType string 14 | expectedLiteral string 15 | }{ 16 | {LET, "let"}, 17 | {MULTIPLY, "*"}, 18 | {ASSIGN, "="}, 19 | {NUMBER, "3"}, 20 | {PLUS, "+"}, 21 | {NUMBER, "4"}, 22 | {MULTIPLY, "*"}, 23 | {NUMBER, "5"}, 24 | {MINUS, "-"}, 25 | {NUMBER, "1"}, 26 | {DIVIDE, "/"}, 27 | {NUMBER, "2"}, 28 | {EOF, ""}, 29 | } 30 | 31 | l := NewLexer("LEt * = 3 + 4 * 5 - 1 / 2") 32 | 33 | for i, tt := range tests { 34 | tok := l.Next() 35 | if tok.Type != tt.expectedType { 36 | t.Fatalf("tests[%d] - tokentype wrong, expected=%q, got=%q", i, tt.expectedType, tok.Type) 37 | } 38 | if fmt.Sprintf("%v", tok.Value) != tt.expectedLiteral { 39 | t.Fatalf("tests[%d] - Literal wrong, expected=%q, got=%q", i, tt.expectedLiteral, tok.Value) 40 | } 41 | } 42 | 43 | } 44 | 45 | // Test we can parse numbers correctly 46 | func TestNumbers(t *testing.T) { 47 | 48 | // 49 | // We're going to create a number so big that it cannot 50 | // be parsed by strconv.ParseFloat. 51 | // 52 | // Maximum value. 53 | // 54 | fmax := 1.7976931348623157e+308 55 | 56 | // Now, as a string. 57 | fmaxStr := fmt.Sprintf("%f", fmax) 58 | 59 | // Add a prefix to make it too big. 60 | fmaxStr = "9999" + fmaxStr 61 | 62 | tests := []struct { 63 | input string 64 | error bool 65 | errMsg string 66 | }{ 67 | {"-3", false, ""}, 68 | {".1", false, ""}, 69 | {".1.1", true, "too many"}, 70 | {"$", true, "unknown character"}, 71 | {fmaxStr, true, "failed to parse number"}, 72 | } 73 | 74 | for n, test := range tests { 75 | 76 | l := NewLexer(test.input) 77 | 78 | // Loop over all tokens and see if we found an error 79 | err := "" 80 | 81 | tok := l.Next() 82 | for tok.Type != EOF { 83 | if tok.Type == ERROR { 84 | err = tok.Value.(string) 85 | } 86 | tok = l.Next() 87 | 88 | } 89 | 90 | if test.error { 91 | if err == "" { 92 | t.Fatalf("tests[%d] %s - expected error, got none", n, test.input) 93 | } 94 | if !strings.Contains(err, test.errMsg) { 95 | t.Fatalf("expected error to match '%s', but got '%s'", test.errMsg, err) 96 | } 97 | } else { 98 | if err != "" { 99 | t.Fatalf("tests[%d] %s - didn't expect error, got %s", n, test.input, err) 100 | } 101 | } 102 | } 103 | 104 | } 105 | 106 | // TestIssue15 confirms https://github.com/skx/sysbox/issues/15 is closed. 107 | func TestIssue15(t *testing.T) { 108 | tests := []struct { 109 | expectedType string 110 | expectedLiteral string 111 | }{ 112 | {LET, "let"}, 113 | {IDENT, "b"}, 114 | {ASSIGN, "="}, 115 | {NUMBER, "1"}, 116 | {LPAREN, "("}, 117 | {IDENT, "b"}, 118 | {MINUS, "-"}, 119 | {IDENT, "b"}, 120 | {RPAREN, ")"}, 121 | {EOF, ""}, 122 | } 123 | 124 | l := NewLexer("LeT b = 1; ( b -b)") 125 | 126 | for i, tt := range tests { 127 | tok := l.Next() 128 | if tok.Type != tt.expectedType { 129 | t.Fatalf("tests[%d] - tokentype wrong, expected=%q, got=%q", i, tt.expectedType, tok.Type) 130 | } 131 | if fmt.Sprintf("%v", tok.Value) != tt.expectedLiteral { 132 | t.Fatalf("tests[%d] - Literal wrong, expected=%q, got=%q", i, tt.expectedLiteral, tok.Value) 133 | } 134 | } 135 | } 136 | 137 | func TestNumeric(t *testing.T) { 138 | 139 | lexer := NewLexer("bogus stuff") 140 | 141 | ok := lexer.isNumberComponent('-', true) 142 | if !ok { 143 | t.Fatalf("leading '-' wasn't handled") 144 | } 145 | 146 | ok = lexer.isNumberComponent('-', false) 147 | if ok { 148 | t.Fatalf("'-' isn't valid unless at the start of a number") 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /chooseui/chooseui.go: -------------------------------------------------------------------------------- 1 | // Package chooseui presents a simple console-based UI. 2 | // 3 | // The user-interface is constructed with an array of strings, 4 | // and will allow the user to choose one of them. The list may 5 | // be filtered, and the user can cancel if they wish. 6 | package chooseui 7 | 8 | import ( 9 | "sort" 10 | "strings" 11 | 12 | "github.com/gdamore/tcell/v2" 13 | "github.com/rivo/tview" 14 | ) 15 | 16 | // ChooseUI is the structure for holding our state. 17 | type ChooseUI struct { 18 | 19 | // The items the user will choose from. 20 | Choices []string 21 | 22 | // The users' choice. 23 | chosen string 24 | 25 | // app is the global application 26 | app *tview.Application 27 | 28 | // list contains the global list of text-entries. 29 | list *tview.List 30 | 31 | // inputField contains the global text-input field. 32 | inputField *tview.InputField 33 | } 34 | 35 | // New creates a new UI, allowing the user to select from the available options. 36 | func New(choices []string) *ChooseUI { 37 | sort.Strings(choices) 38 | return &ChooseUI{Choices: choices} 39 | } 40 | 41 | // SetupUI configures the UI. 42 | func (ui *ChooseUI) SetupUI() { 43 | 44 | // 45 | // Create the console-GUI application. 46 | // 47 | ui.app = tview.NewApplication() 48 | 49 | // 50 | // Create a list to hold our files. 51 | // 52 | ui.list = tview.NewList() 53 | ui.list.ShowSecondaryText(false) 54 | ui.list.SetWrapAround(false) 55 | 56 | // 57 | // Add all the choices to it. 58 | // 59 | for _, entry := range ui.Choices { 60 | ui.list.AddItem(entry, "", ' ', nil) 61 | } 62 | 63 | // 64 | // Create a filter input-view 65 | // 66 | ui.inputField = tview.NewInputField(). 67 | SetLabel("Filter: "). 68 | SetDoneFunc(func(key tcell.Key) { 69 | if key == tcell.KeyEnter { 70 | 71 | // get the selected index 72 | selected := ui.list.GetCurrentItem() 73 | 74 | // less than the entry count? 75 | if ui.list.GetItemCount() > 0 { 76 | ui.chosen, _ = ui.list.GetItemText(selected) 77 | } 78 | ui.app.Stop() 79 | } 80 | }) 81 | 82 | // 83 | // Setup the filter-function, to filter the list to 84 | // only matches present in the input-field 85 | // 86 | ui.inputField.SetAutocompleteFunc(func(currentText string) (entries []string) { 87 | // Get text 88 | input := strings.TrimSpace(currentText) 89 | 90 | // empty? All items should be visible 91 | if input == "" { 92 | ui.list.Clear() 93 | for _, entry := range ui.Choices { 94 | ui.list.AddItem(entry, "", ' ', nil) 95 | } 96 | return 97 | } 98 | 99 | // Otherwise filter by input 100 | input = strings.ToLower(input) 101 | ui.list.Clear() 102 | for _, entry := range ui.Choices { 103 | if strings.Contains(strings.ToLower(entry), input) { 104 | ui.list.AddItem(entry, "", ' ', nil) 105 | } 106 | } 107 | 108 | return 109 | }) 110 | 111 | // 112 | // Help text 113 | // 114 | help := tview.NewBox().SetBorder(true).SetTitle("TAB to switch focus, ENTER to select, ESC to cancel, arrows/etc to move") 115 | 116 | // 117 | // Create a layout grid, add the filter-box and the list. 118 | // 119 | grid := tview.NewFlex().SetFullScreen(true).SetDirection(tview.FlexRow) 120 | grid.AddItem(ui.inputField, 1, 0, true) 121 | grid.AddItem(ui.list, 0, 1, false) 122 | grid.AddItem(help, 2, 1, false) 123 | 124 | ui.app.SetRoot(grid, true).SetFocus(grid).EnableMouse(true) 125 | 126 | } 127 | 128 | // SetupKeyBinding installs the global captures, and list-specific keybindings. 129 | func (ui *ChooseUI) SetupKeyBinding() { 130 | 131 | // 132 | // If the user presses return in the list then choose that item. 133 | // 134 | ui.list.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey { 135 | if event.Key() == tcell.KeyEnter { 136 | selected := ui.list.GetCurrentItem() 137 | ui.chosen, _ = ui.list.GetItemText(selected) 138 | ui.app.Stop() 139 | } 140 | return event 141 | }) 142 | 143 | // 144 | // Global keyboard handler, use "TAB" to switch focus. 145 | // 146 | // Arrows and HOME/END work as expected regardless of focus-state 147 | // 148 | ui.app.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey { 149 | switch event.Key() { 150 | 151 | // Home 152 | case tcell.KeyHome: 153 | ui.list.SetCurrentItem(0) 154 | 155 | // End 156 | case tcell.KeyEnd: 157 | ui.list.SetCurrentItem(ui.list.GetItemCount()) 158 | 159 | // Up arrow 160 | case tcell.KeyUp: 161 | selected := ui.list.GetCurrentItem() 162 | if selected > 0 { 163 | selected-- 164 | } else { 165 | selected = ui.list.GetItemCount() 166 | } 167 | ui.list.SetCurrentItem(selected) 168 | return nil 169 | 170 | // Down arrow 171 | case tcell.KeyDown: 172 | selected := ui.list.GetCurrentItem() 173 | selected++ 174 | ui.list.SetCurrentItem(selected) 175 | return nil 176 | 177 | // TAB 178 | case tcell.KeyTab, tcell.KeyBacktab: 179 | if ui.list.HasFocus() { 180 | ui.app.SetFocus(ui.inputField) 181 | } else { 182 | ui.app.SetFocus(ui.list) 183 | } 184 | return nil 185 | 186 | // Escape 187 | case tcell.KeyEscape: 188 | ui.app.Stop() 189 | } 190 | return event 191 | }) 192 | 193 | } 194 | 195 | // Choose launches our user interface. 196 | func (ui *ChooseUI) Choose() string { 197 | 198 | ui.SetupUI() 199 | 200 | ui.SetupKeyBinding() 201 | 202 | // 203 | // Launch the application. 204 | // 205 | err := ui.app.Run() 206 | if err != nil { 207 | panic(err) 208 | } 209 | 210 | // 211 | // Return the choice 212 | // 213 | return ui.chosen 214 | } 215 | -------------------------------------------------------------------------------- /cmd_calc.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "strings" 7 | 8 | "github.com/peterh/liner" 9 | "github.com/skx/subcommands" 10 | "github.com/skx/sysbox/calc" 11 | ) 12 | 13 | // Structure for our options and state. 14 | type calcCommand struct { 15 | 16 | // We embed the NoFlags option, because we accept no command-line flags. 17 | subcommands.NoFlags 18 | } 19 | 20 | // Info returns the name of this subcommand. 21 | func (c *calcCommand) Info() (string, string) { 22 | return "calc", `A simple (floating-point) calculator. 23 | 24 | Details: 25 | 26 | This command allows you to evaluate simple mathematical operations, 27 | with support for floating-point operations - something the standard 28 | 'expr' command does not support. 29 | 30 | Example: 31 | 32 | $ sysbox calc 3 + 3 33 | $ sysbox calc '1 / 3 * 9' 34 | 35 | Note here we can join arguments, or accept a quoted string. The arguments 36 | must be quoted if you use '*' because otherwise the shell's globbing would 37 | cause surprises. 38 | 39 | Repl: 40 | 41 | If you execute this command with no arguments you'll be dropped into a REPL 42 | environment. This environment is almost 100% identical to the non-interactive 43 | use, with the exception that you can define variables: 44 | 45 | $ sysbox calc 46 | calc> let a = 3 47 | 3 48 | calc> a * 3 49 | 9 50 | calc> a / 9 51 | 0.3333 52 | calc> exit 53 | 54 | If you prefer you can handle assignments without "let": 55 | 56 | calc> a = 1; b = 2 ; c = 3 57 | 3 58 | calc> a + b * c 59 | 7 60 | calc> exit 61 | 62 | The result of the previous calculation is always stored in the variable 'result': 63 | 64 | calc> 1 / 3 65 | 0.3333 66 | calc> result * 3 67 | 1 68 | ` 69 | } 70 | 71 | // Show the result of a calculation 72 | func (c *calcCommand) showResult(out *calc.Token) error { 73 | 74 | if out == nil { 75 | return fmt.Errorf("nil result") 76 | } 77 | if out.Type == calc.ERROR { 78 | return fmt.Errorf("%s", out.Value.(string)) 79 | } 80 | if out.Type != calc.NUMBER { 81 | return fmt.Errorf("unexpected output (not a number): %v", out) 82 | } 83 | 84 | // 85 | // Show the result as an int, if possible. 86 | // 87 | result := out.Value.(float64) 88 | if float64(int(result)) == result { 89 | fmt.Printf("%d\n", int(result)) 90 | return nil 91 | } 92 | 93 | // 94 | // strip trailing "0" 95 | // 96 | // First convert to string, then remove each 97 | // final zero. 98 | output := fmt.Sprintf("%f", result) 99 | for strings.HasSuffix(output, "0") { 100 | output = strings.TrimSuffix(output, "0") 101 | } 102 | fmt.Printf("%s\n", output) 103 | return nil 104 | } 105 | 106 | // Execute is invoked if the user specifies `calc` as the subcommand. 107 | func (c *calcCommand) Execute(args []string) int { 108 | 109 | // 110 | // Join all arguments, in case we have been given "3", "+", "4". 111 | // 112 | input := "" 113 | 114 | for _, arg := range args { 115 | input += arg 116 | input += " " 117 | } 118 | 119 | // 120 | // Create a new evaluator 121 | // 122 | cal := calc.New() 123 | 124 | // 125 | // If we have no arguments then we're in the repl. 126 | // 127 | // Otherwise we process the input. 128 | // 129 | if len(input) > 0 { 130 | 131 | // 132 | // Load the script 133 | // 134 | cal.Load(input) 135 | 136 | // 137 | // Run it. 138 | // 139 | out := cal.Run() 140 | 141 | // 142 | // Show the result. 143 | // 144 | err := c.showResult(out) 145 | if err != nil { 146 | fmt.Printf("error: %s\n", err) 147 | return 1 148 | } 149 | 150 | return 0 151 | } 152 | 153 | // 154 | // Repl uses command-history 155 | // 156 | line := liner.NewLiner() 157 | defer line.Close() 158 | 159 | // 160 | // Tab completion 161 | // 162 | complete := []string{"exit", "help", "result", "quit"} 163 | 164 | line.SetCompleter(func(line string) (c []string) { 165 | for _, n := range complete { 166 | if strings.HasPrefix(n, strings.ToLower(line)) { 167 | c = append(c, n) 168 | } 169 | } 170 | return 171 | }) 172 | // 173 | // Ctrl-C will abort input of a line, not the whole program. 174 | // 175 | line.SetCtrlCAborts(false) 176 | 177 | // 178 | // Loop until we should stop 179 | // 180 | run := true 181 | for run { 182 | 183 | input, err := line.Prompt("calc>") 184 | if err == nil { 185 | 186 | // 187 | // Trim the input 188 | // 189 | input = strings.TrimSpace(input) 190 | 191 | // 192 | // Exit ? 193 | // 194 | if strings.HasPrefix(input, "exit") || 195 | strings.HasPrefix(input, "quit") { 196 | run = false 197 | continue 198 | } 199 | 200 | // 201 | // Help ? 202 | // 203 | if strings.HasPrefix(input, "help") { 204 | _, txt := c.Info() 205 | fmt.Printf("%s\n", txt) 206 | continue 207 | } 208 | 209 | // 210 | // Is the input empty? 211 | // 212 | if input == "" { 213 | continue 214 | } 215 | 216 | // 217 | // Load the script 218 | // 219 | cal.Load(input) 220 | 221 | // 222 | // Run it. 223 | // 224 | out := cal.Run() 225 | 226 | // 227 | // Show the result. 228 | // 229 | err = c.showResult(out) 230 | if err != nil { 231 | fmt.Printf("error: %s\n", err) 232 | } 233 | 234 | // 235 | // Add the input to our history. 236 | // 237 | // NOTE: Our history is deliberately not persisted. 238 | // 239 | line.AppendHistory(input) 240 | } 241 | 242 | // Ctrl-d 243 | if io.EOF == err { 244 | run = false 245 | fmt.Printf("\n") 246 | } 247 | 248 | } 249 | 250 | // 251 | // All done 252 | // 253 | return 0 254 | } 255 | -------------------------------------------------------------------------------- /cmd_choose_file.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "os" 7 | "os/exec" 8 | "path/filepath" 9 | "strings" 10 | 11 | "github.com/skx/sysbox/chooseui" 12 | "github.com/skx/sysbox/templatedcmd" 13 | ) 14 | 15 | // Structure for our options and state. 16 | type chooseFileCommand struct { 17 | 18 | // Command to execute 19 | exec string 20 | 21 | // Filenames we'll let the user choose between 22 | files []string 23 | } 24 | 25 | // Arguments adds per-command args to the object. 26 | func (cf *chooseFileCommand) Arguments(f *flag.FlagSet) { 27 | if cf != nil { 28 | f.StringVar(&cf.exec, "execute", "", "Command to execute once a selection has been made") 29 | } 30 | } 31 | 32 | // Info returns the name of this subcommand. 33 | func (cf *chooseFileCommand) Info() (string, string) { 34 | return "choose-file", `Choose a file, interactively. 35 | 36 | Details: 37 | 38 | This command presents a directory view, showing you all the files beneath 39 | the named directory. You can navigate with the keyboard, and press RETURN 40 | to select a file. 41 | 42 | Optionally you can press TAB to filter the list via an input field. 43 | 44 | Uses: 45 | 46 | This is ideal for choosing videos, roms, etc. For example launch a 47 | video file, interactively: 48 | 49 | $ xine "$(sysbox choose-file ~/Videos)" 50 | $ sysbox choose-file -execute="xine {}" ~/Videos 51 | 52 | See also 'sysbox help choose-stdin'.` 53 | } 54 | 55 | // Execute is invoked if the user specifies `choose-file` as the subcommand. 56 | func (cf *chooseFileCommand) Execute(args []string) int { 57 | 58 | // 59 | // Get our starting directory 60 | // 61 | dir := "." 62 | if len(args) > 0 { 63 | dir = args[0] 64 | } 65 | 66 | // 67 | // Find files 68 | // 69 | err := filepath.Walk(dir, 70 | func(path string, info os.FileInfo, err error) error { 71 | 72 | // Null info? That probably means that the 73 | // destination we're trying to walk doesn't exist. 74 | if info == nil { 75 | return nil 76 | } 77 | 78 | // We'll add anything that isn't a directory 79 | if !info.IsDir() { 80 | if !strings.Contains(path, "/.") && !strings.HasPrefix(path, ".") { 81 | cf.files = append(cf.files, path) 82 | } 83 | } 84 | return nil 85 | }) 86 | 87 | if err != nil { 88 | fmt.Printf("error walking %s: %s\n", dir, err.Error()) 89 | return 1 90 | } 91 | if len(cf.files) < 1 { 92 | fmt.Printf("Failed to find any files beneath %s\n", dir) 93 | return 1 94 | } 95 | 96 | // 97 | // Launch the UI 98 | // 99 | chooser := chooseui.New(cf.files) 100 | choice := chooser.Choose() 101 | 102 | // 103 | // Did something get chosen? If not terminate 104 | // 105 | if choice == "" { 106 | 107 | return 1 108 | } 109 | 110 | // 111 | // Are we executing? 112 | // 113 | if cf.exec != "" { 114 | 115 | // 116 | // Split into command and arguments 117 | // 118 | run := templatedcmd.Expand(cf.exec, choice, "") 119 | 120 | // 121 | // Run it. 122 | // 123 | cmd := exec.Command(run[0], run[1:]...) 124 | out, errr := cmd.CombinedOutput() 125 | if errr != nil { 126 | fmt.Printf("Error running '%v': %s\n", run, errr.Error()) 127 | return 1 128 | } 129 | 130 | // 131 | // And we're done 132 | // 133 | fmt.Printf("%s\n", out) 134 | return 0 135 | 136 | } 137 | 138 | // 139 | // We're not executing, so show the user's choice 140 | // 141 | fmt.Printf("%s\n", choice) 142 | return 0 143 | } 144 | -------------------------------------------------------------------------------- /cmd_choose_stdin.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "flag" 6 | "fmt" 7 | "os" 8 | "os/exec" 9 | "strings" 10 | 11 | "github.com/skx/sysbox/chooseui" 12 | "github.com/skx/sysbox/templatedcmd" 13 | ) 14 | 15 | // Structure for our options and state. 16 | type chooseSTDINCommand struct { 17 | // Command to execute 18 | exec string 19 | 20 | // Filenames we'll let the user choose between 21 | stdin []string 22 | } 23 | 24 | // Arguments adds per-command args to the object. 25 | func (cs *chooseSTDINCommand) Arguments(f *flag.FlagSet) { 26 | f.StringVar(&cs.exec, "execute", "", "Command to execute once a selection has been made") 27 | } 28 | 29 | // Info returns the name of this subcommand. 30 | func (cs *chooseSTDINCommand) Info() (string, string) { 31 | return "choose-stdin", `Choose an item from STDIN, interactively. 32 | 33 | Details: 34 | 35 | This command presents a simple UI, showing all the lines read from STDIN. 36 | 37 | You can navigate with the keyboard, and press RETURN to select an entry. 38 | 39 | Optionally you can press TAB to filter the list via an input field. 40 | 41 | Uses: 42 | 43 | This is ideal for choosing videos, roms, etc. For example launch the 44 | given video file: 45 | 46 | $ find . -name '*.avi' -print | sysbox choose-stdin -exec 'xine "{}"' 47 | 48 | See also 'sysbox help choose-file'.` 49 | } 50 | 51 | // Execute is invoked if the user specifies `choose-stdin` as the subcommand. 52 | func (cs *chooseSTDINCommand) Execute(args []string) int { 53 | 54 | // 55 | // Prepare to read line-by-line 56 | // 57 | scanner := bufio.NewReader(os.Stdin) 58 | 59 | // 60 | // Read a line 61 | // 62 | line, err := scanner.ReadString(byte('\n')) 63 | for err == nil && line != "" { 64 | 65 | // 66 | // Remove any leading/trailing whitespace 67 | // 68 | line = strings.TrimSpace(line) 69 | 70 | // 71 | // Save this away 72 | // 73 | cs.stdin = append(cs.stdin, line) 74 | 75 | // 76 | // Loop again 77 | // 78 | line, err = scanner.ReadString(byte('\n')) 79 | } 80 | 81 | // 82 | // Launch the UI 83 | // 84 | chooser := chooseui.New(cs.stdin) 85 | choice := chooser.Choose() 86 | 87 | // 88 | // Did something get chosen? If not terminate 89 | // 90 | if choice == "" { 91 | 92 | return 1 93 | } 94 | 95 | // 96 | // Are we executing? 97 | // 98 | if cs.exec != "" { 99 | 100 | // 101 | // Split into command and arguments 102 | // 103 | run := templatedcmd.Expand(cs.exec, choice, "") 104 | 105 | // 106 | // Run it. 107 | // 108 | cmd := exec.Command(run[0], run[1:]...) 109 | out, errr := cmd.CombinedOutput() 110 | if errr != nil { 111 | fmt.Printf("Error running '%v': %s\n", run, errr.Error()) 112 | return 1 113 | } 114 | 115 | // 116 | // And we're done 117 | // 118 | fmt.Printf("%s\n", out) 119 | return 0 120 | 121 | } 122 | 123 | // 124 | // We're not running a command, so output the user's choice 125 | // 126 | fmt.Printf("%s\n", choice) 127 | return 0 128 | } 129 | -------------------------------------------------------------------------------- /cmd_chronic.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "os/exec" 7 | "syscall" 8 | 9 | "github.com/skx/subcommands" 10 | ) 11 | 12 | // Structure for our options and state. 13 | type chronicCommand struct { 14 | 15 | // We embed the NoFlags option, because we accept no command-line flags. 16 | subcommands.NoFlags 17 | } 18 | 19 | // Info returns the name of this subcommand. 20 | func (c *chronicCommand) Info() (string, string) { 21 | return "chronic", `Run a command quietly, if it succeeds. 22 | 23 | Details: 24 | 25 | The chronic command allows you to execute a program, and hide the output 26 | if the command succeeds. 27 | 28 | The ideal use-case is for wrapping cronjobs, where you don't care about the 29 | output unless the execution fails. 30 | 31 | Example: 32 | 33 | Compare the output of these two commands: 34 | 35 | $ sysbox chronic ls 36 | $ 37 | 38 | $ sysbox chronic ls /missing/dir 39 | ls: cannot access '/missing/file': No such file or directory 40 | ` 41 | } 42 | 43 | // RunCommand is a helper to run a command, returning output and the exit-code. 44 | func (c *chronicCommand) RunCommand(command []string) (stdout string, stderr string, exitCode int) { 45 | var outbuf, errbuf bytes.Buffer 46 | cmd := exec.Command(command[0], command[1:]...) 47 | cmd.Stdout = &outbuf 48 | cmd.Stderr = &errbuf 49 | 50 | err := cmd.Run() 51 | stdout = outbuf.String() 52 | stderr = errbuf.String() 53 | 54 | if err != nil { 55 | // try to get the exit code 56 | if exitError, ok := err.(*exec.ExitError); ok { 57 | ws := exitError.Sys().(syscall.WaitStatus) 58 | exitCode = ws.ExitStatus() 59 | } else { 60 | // This will happen (in OSX) if `name` is not 61 | // available in $PATH, in this situation, exit 62 | // code could not be get, and stderr will be 63 | // empty string very likely, so we use the default 64 | // fail code, and format err to string and set to stderr 65 | exitCode = 1 66 | if stderr == "" { 67 | stderr = err.Error() 68 | } 69 | } 70 | } else { 71 | // success, exitCode should be 0 if go is ok 72 | ws := cmd.ProcessState.Sys().(syscall.WaitStatus) 73 | exitCode = ws.ExitStatus() 74 | } 75 | return stdout, stderr, exitCode 76 | } 77 | 78 | // Execute is invoked if the user specifies `chronic` as the subcommand. 79 | func (c *chronicCommand) Execute(args []string) int { 80 | 81 | if len(args) <= 0 { 82 | fmt.Printf("Usage: chronic command to execute ..\n") 83 | return 1 84 | } 85 | 86 | stdout, stderr, exit := c.RunCommand(args) 87 | if exit == 0 { 88 | return 0 89 | } 90 | 91 | fmt.Printf("%q exited with status code %d\n", args, exit) 92 | if len(stdout) > 0 { 93 | fmt.Println(stdout) 94 | } 95 | if len(stderr) > 0 { 96 | fmt.Println(stderr) 97 | } 98 | return exit 99 | } 100 | -------------------------------------------------------------------------------- /cmd_collapse.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "fmt" 6 | "os" 7 | "strings" 8 | 9 | "github.com/skx/subcommands" 10 | ) 11 | 12 | // Structure for our options and state. 13 | type collapseCommand struct { 14 | 15 | // We embed the NoFlags option, because we accept no command-line flags. 16 | subcommands.NoFlags 17 | } 18 | 19 | // Info returns the name of this subcommand. 20 | func (c *collapseCommand) Info() (string, string) { 21 | return "collapse", `Remove whitespace from input. 22 | 23 | Details: 24 | 25 | This command reads input and removes all leading and trailing whitespace 26 | from it. Empty lines are also discarded.` 27 | } 28 | 29 | // Execute is invoked if the user specifies `collapse` as the subcommand. 30 | func (c *collapseCommand) Execute(args []string) int { 31 | 32 | scanner := bufio.NewScanner(os.Stdin) 33 | 34 | for scanner.Scan() { 35 | line := scanner.Text() 36 | line = strings.TrimSpace(line) 37 | if len(line) > 0 { 38 | fmt.Println(line) 39 | } 40 | } 41 | 42 | if err := scanner.Err(); err != nil { 43 | fmt.Printf("Error: %s\n", err.Error()) 44 | return 1 45 | } 46 | 47 | return 0 48 | } 49 | -------------------------------------------------------------------------------- /cmd_comments.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "os" 7 | "regexp" 8 | "sort" 9 | "strings" 10 | ) 11 | 12 | // Comment is a structure to hold a language/syntax for comments. 13 | // 14 | // A comment is denoted as the content between a start-marker, and an 15 | // end-marker. For single-line comments we define the end-marker as 16 | // being a newline. 17 | type Comment struct { 18 | 19 | // The text which denotes the start of a comment. 20 | // 21 | // For C++ this might be `/*`, for a shell-script it might be `#`. 22 | start string 23 | 24 | // The text which denotes the end of a comment. 25 | // 26 | // For C++ this might be `*/`, for a shell-script it might be `\n`. 27 | end string 28 | 29 | // Some comment-openers are only valid at the start of a line. 30 | bol bool 31 | } 32 | 33 | // Structure for our options and state. 34 | type commentsCommand struct { 35 | 36 | // The styles of comments to be enabled, as set by the command-line. 37 | style string 38 | 39 | // Pretty-print the comments? 40 | pretty bool 41 | 42 | // The comments we're matching 43 | patterns []Comment 44 | } 45 | 46 | // Arguments adds per-command args to the object. 47 | func (cc *commentsCommand) Arguments(f *flag.FlagSet) { 48 | f.StringVar(&cc.style, "style", "c,cpp", "A comma-separated list of the comment-styles to use") 49 | f.BoolVar(&cc.pretty, "pretty", false, "Reformat comments for readability") 50 | 51 | } 52 | 53 | // Info returns the name of this subcommand. 54 | func (cc *commentsCommand) Info() (string, string) { 55 | return "comments", `Output the comments contained in the given file. 56 | 57 | Details: 58 | 59 | This naive command outputs the comments which are included in the specified 60 | filename(s). This is useful if you wish to run spell-checkers, etc. 61 | 62 | There is support for outputting single-line and multi-line comments for C, 63 | C++, Lua, and Golang. Additional options are welcome. By default C, and 64 | C++ are enabled. To only use Lua comments you could run: 65 | 66 | $ sysbox comments --style=lua *.lua` 67 | } 68 | 69 | // showComment writes the comment to the console, after optionally tidying 70 | func (cc *commentsCommand) showComment(comment string) { 71 | if cc.pretty { 72 | // Remove newlines 73 | comment = strings.Replace(comment, "\n", " ", -1) 74 | 75 | // Remove " * " 76 | comment = strings.Replace(comment, " * ", " ", -1) 77 | 78 | // Collapse adjacent spaces 79 | comment = strings.Join(strings.Fields(comment), " ") 80 | 81 | // Skip empty comments; i.e. just literal matches of 82 | // the opening pattern. 83 | for _, pattern := range cc.patterns { 84 | if comment == pattern.start { 85 | return 86 | } 87 | } 88 | } 89 | 90 | // Remove trailing newline, so we can safely add one 91 | comment = strings.TrimSuffix(comment, "\n") 92 | fmt.Printf("%s\n", comment) 93 | } 94 | 95 | // dumpComments dumps the comments from the given file. 96 | func (cc *commentsCommand) dumpComments(filename string) { 97 | 98 | // Read the content 99 | content, err := os.ReadFile(filename) 100 | if err != nil { 101 | fmt.Printf("error reading %s: %s\n", filename, err.Error()) 102 | return 103 | } 104 | 105 | // Convert our internal patterns to a series of regular expressions. 106 | var r []*regexp.Regexp 107 | for _, pattern := range cc.patterns { 108 | reg := "(?s)" 109 | 110 | if pattern.bol { 111 | reg += "^" 112 | } 113 | 114 | reg += regexp.QuoteMeta(pattern.start) 115 | reg += "(.*?)" 116 | reg += regexp.QuoteMeta(pattern.end) 117 | 118 | fmt.Printf("%v\n", reg) 119 | r = append(r, regexp.MustCompile(reg)) 120 | } 121 | 122 | // Now for each regexp do the matching over the whole input. 123 | for _, re := range r { 124 | out := re.FindAllSubmatch(content, -1) 125 | for _, match := range out { 126 | cc.showComment(string(match[0])) 127 | } 128 | } 129 | 130 | } 131 | 132 | // Execute is invoked if the user specifies `comments` as the subcommand. 133 | func (cc *commentsCommand) Execute(args []string) int { 134 | 135 | // Map of known patterns, by name 136 | known := make(map[string][]Comment) 137 | 138 | // Populate with the patterns. 139 | known["ada"] = []Comment{{start: "--", end: "\n"}} 140 | known["apl"] = []Comment{{start: "⍝", end: "\n"}} 141 | known["applescript"] = []Comment{{start: "(*", end: "*)"}, 142 | {start: "--", end: "\n"}} 143 | known["asm"] = []Comment{{start: ";", end: "\n"}} 144 | known["basic"] = []Comment{{start: "REM", end: "\n"}} 145 | known["c"] = []Comment{{start: "//", end: "\n"}} 146 | known["coldfusion"] = []Comment{{start: ""}} 147 | known["cpp"] = []Comment{{start: "/*", end: "*/"}} 148 | known["fortran"] = []Comment{{start: "!", end: "\n", bol: true}} 149 | known["go"] = []Comment{{start: "/*", end: "*/"}, 150 | {start: "//", end: "\n"}, 151 | } 152 | known["html"] = []Comment{{start: ""}} 153 | known["haskell"] = []Comment{{start: "{-", end: "-}"}, 154 | {start: "--", end: "\n"}} 155 | known["lisp"] = []Comment{{start: ";", end: "\n"}} 156 | known["java"] = []Comment{{start: "/*", end: "*/"}, 157 | {start: "//", end: "\n"}} 158 | known["javascript"] = []Comment{{start: "/*", end: "*/"}, 159 | {start: "//", end: "\n"}} 160 | known["lua"] = []Comment{{start: "--[[", end: "--]]"}, 161 | {start: "-- ", end: "\n"}} 162 | known["matlab"] = []Comment{{start: "%{", end: "%}"}, 163 | {start: "% ", end: "\n"}} 164 | known["pascal"] = []Comment{{start: "(*", end: "*)"}} 165 | known["perl"] = []Comment{{start: "#", end: "\n"}} 166 | known["php"] = []Comment{{start: "/*", end: "*/"}, 167 | {start: "//", end: "\n"}, 168 | {start: "#", end: "\n"}, 169 | } 170 | known["python"] = []Comment{{start: "#", end: "\n"}} 171 | known["ruby"] = []Comment{{start: "#", end: "\n"}} 172 | known["shell"] = []Comment{{start: "#", end: "\n"}} 173 | known["swift"] = []Comment{{start: "/*", end: "*/"}, 174 | {start: "//", end: "\n"}} 175 | known["sql"] = []Comment{{start: "--", end: "\n"}} 176 | known["xml"] = []Comment{{start: ""}} 177 | 178 | // Ensure we have at least one filename specified. 179 | if len(args) <= 0 { 180 | fmt.Printf("Usage: comments file1 [file2] ..[argN]\n") 181 | return 1 182 | } 183 | 184 | // Load the patterns the user selected. 185 | for _, kind := range strings.Split(cc.style, ",") { 186 | 187 | // Lookup the choice 188 | pat, ok := known[kind] 189 | 190 | // Not found? That's an error 191 | if !ok { 192 | fmt.Printf("Unknown style %s, valid options include:\n", kind) 193 | 194 | keys := make([]string, 0, len(known)) 195 | for k := range known { 196 | keys = append(keys, k) 197 | } 198 | sort.Strings(keys) 199 | 200 | for _, k := range keys { 201 | fmt.Printf("\t%s\n", k) 202 | } 203 | return 1 204 | } 205 | 206 | // Otherwise add it to the list. 207 | cc.patterns = append(cc.patterns, pat...) 208 | } 209 | 210 | // Now process the input file(s) 211 | for _, file := range args { 212 | cc.dumpComments(file) 213 | } 214 | 215 | // All done. 216 | return 0 217 | } 218 | -------------------------------------------------------------------------------- /cmd_cpp.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "fmt" 6 | "os" 7 | "os/exec" 8 | "regexp" 9 | 10 | "github.com/skx/subcommands" 11 | ) 12 | 13 | // Structure for our options and state. 14 | type cppCommand struct { 15 | 16 | // We embed the NoFlags option, because we accept no command-line flags. 17 | subcommands.NoFlags 18 | 19 | // regular expression to find include-statements 20 | include *regexp.Regexp 21 | 22 | // regular expression to find exec-statements 23 | exec *regexp.Regexp 24 | } 25 | 26 | // Info returns the name of this subcommand. 27 | func (c *cppCommand) Info() (string, string) { 28 | return "cpp", `Trivial CPP-like preprocessor. 29 | 30 | Details: 31 | 32 | This command is a minimal implementation of something that looks a little 33 | like the standard C preprocessor, cpp. 34 | 35 | We only support two directives at the moment: 36 | 37 | * #include 38 | * You can use #include "file/path" if you prefer 39 | * #execute command argument1 .. argument2 40 | * The command is executed via the shell, so you can pipe, etc. 41 | 42 | Example: 43 | 44 | Given the following file: 45 | 46 | before 47 | #include "/etc/passwd" 48 | #execute /bin/ls -l | grep " 2 " 49 | after 50 | 51 | You can expand that via either of these two commands: 52 | 53 | $ sysbox cpp file.in 54 | $ cat file.in | sysbox cpp 55 | ` 56 | } 57 | 58 | // Process the contents of the given reader. 59 | func (c *cppCommand) process(reader *bufio.Reader) { 60 | 61 | // 62 | // Read line by line. 63 | // 64 | // Usually we'd use bufio.Scanner, however that can 65 | // report problems with lines that are too long: 66 | // 67 | // Error: bufio.Scanner: token too long 68 | // 69 | // Instead we use the bufio.ReadString method to avoid it. 70 | // 71 | line, err := reader.ReadString(byte('\n')) 72 | for err == nil { 73 | 74 | // 75 | // Do we have an #include-line? 76 | // 77 | matches := c.include.FindAllStringSubmatch(line, -1) 78 | for _, v := range matches { 79 | if len(v) > 0 { 80 | 81 | file := v[1] 82 | 83 | // 84 | // Now we should have a file to include 85 | // 86 | dat, derr := os.ReadFile(file) 87 | if derr != nil { 88 | fmt.Printf("error including: %s - %s\n", file, derr.Error()) 89 | return 90 | } 91 | 92 | line = string(dat) 93 | } 94 | } 95 | 96 | // 97 | // Do we have "#execute" ? 98 | // 99 | matches = c.exec.FindAllStringSubmatch(line, -1) 100 | for _, v := range matches { 101 | if len(v) < 1 { 102 | continue 103 | } 104 | 105 | cmd := exec.Command("/bin/bash", "-c", v[1]) 106 | out, derrr := cmd.CombinedOutput() 107 | if derrr != nil { 108 | fmt.Printf("Error running '%v': %s\n", cmd, derrr.Error()) 109 | return 110 | } 111 | 112 | line = string(out) 113 | } 114 | 115 | fmt.Printf("%s", line) 116 | 117 | // Loop again 118 | line, err = reader.ReadString(byte('\n')) 119 | } 120 | } 121 | 122 | // Execute is invoked if the user specifies `cpp` as the subcommand. 123 | func (c *cppCommand) Execute(args []string) int { 124 | 125 | // 126 | // Setup our regular expressions. 127 | // 128 | c.include = regexp.MustCompile("^#\\s*include\\s+[\"<]([^\">]+)[\">]") 129 | c.exec = regexp.MustCompile("^#\\s*execute\\s+([^\n\r]+)[\r\n]$") 130 | 131 | // 132 | // Read from STDIN 133 | // 134 | if len(args) == 0 { 135 | 136 | scanner := bufio.NewReader(os.Stdin) 137 | 138 | c.process(scanner) 139 | 140 | return 0 141 | } 142 | 143 | // 144 | // Otherwise each named file 145 | // 146 | for _, file := range args { 147 | 148 | handle, err := os.Open(file) 149 | if err != nil { 150 | fmt.Printf("error opening %s : %s\n", file, err.Error()) 151 | return 1 152 | } 153 | 154 | reader := bufio.NewReader(handle) 155 | c.process(reader) 156 | } 157 | 158 | return 0 159 | } 160 | -------------------------------------------------------------------------------- /cmd_env_template.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "text/template" 7 | 8 | "github.com/skx/subcommands" 9 | ) 10 | 11 | // Structure for our options and state. 12 | type envTemplateCommand struct { 13 | 14 | // We embed the NoFlags option, because we accept no command-line flags. 15 | subcommands.NoFlags 16 | } 17 | 18 | // Info returns the name of this subcommand. 19 | func (et *envTemplateCommand) Info() (string, string) { 20 | return "env-template", `Populate a template-file with environmental variables. 21 | 22 | Details: 23 | 24 | This command is a slight reworking of the standard 'envsubst' command, 25 | which might not be available upon systems by default, along with extra 26 | support for file-inclusion (which supports the inclusion of other files, 27 | along with extra behavior such as 'grep' and inserting regions of files 28 | between matches of a start/end pair of regular expressions). 29 | 30 | The basic use-case of this sub-command is to allow substituting 31 | environmental variables into simple (golang) template-files. 32 | 33 | However there are extra facilities, as noted above. 34 | 35 | Examples: 36 | 37 | Consider the case where you have a shell with $PATH and $USER available 38 | you might wish to expand those into a file. The file could contain: 39 | 40 | Hello {{env USER}} your path is {{env "PATH"}} 41 | 42 | Expand the contents via: 43 | 44 | $ sysbox env-template path/to/template 45 | 46 | Using the standard golang text/template facilities you can use conditionals 47 | and process variables. For example splitting $PATH into parts: 48 | 49 | // template.in - shows $PATH entries one by one 50 | {{$path := env "PATH"}} 51 | {{$arr := split $path ":"}} 52 | {{range $k, $v := $arr}} 53 | {{$k}} {{$v}} 54 | {{end}} 55 | 56 | 57 | Inclusion Examples: 58 | 59 | The basic case of including a file could be handled like so: 60 | 61 | Before 62 | {{include "/etc/passwd"}} 63 | After 64 | 65 | You can also include only lines matching a particular regular 66 | expression: 67 | 68 | {{grep "/etc/passwd" "^(root|nobody):"}} 69 | 70 | Or lines between a pair of marker (regular expressions): 71 | 72 | {{between "/etc/passwd" "^root" "^bin"}} 73 | 74 | If the input-file contains a '|' prefix it will instead read the output 75 | of running the named command - so you shouldn't process user-submitted 76 | templates, as that is a potential security-risk. 77 | 78 | NOTE: Using 'between' includes the lines that match too, not just the region 79 | between them. If you regard this as a bug please file an issue. 80 | 81 | ` 82 | 83 | } 84 | 85 | // Execute is invoked if the user specifies `env-template` as the subcommand. 86 | func (et *envTemplateCommand) Execute(args []string) int { 87 | 88 | // 89 | // Ensure we have an argument 90 | // 91 | if len(args) < 1 { 92 | fmt.Printf("You must specify the template to expand.\n") 93 | return 1 94 | } 95 | 96 | fail := 0 97 | 98 | for _, file := range args { 99 | err := et.expandFile(file) 100 | if err != nil { 101 | fmt.Printf("error processing %s %s\n", file, err.Error()) 102 | fail = 1 103 | } 104 | } 105 | return fail 106 | } 107 | 108 | // expandFile does the file expansion 109 | func (et *envTemplateCommand) expandFile(path string) error { 110 | 111 | // Load the file 112 | var err error 113 | var content []byte 114 | content, err = os.ReadFile(path) 115 | if err != nil { 116 | return err 117 | } 118 | 119 | // 120 | // Define a helper-function that are available within the 121 | // templates we process. 122 | // 123 | funcMap := template.FuncMap{ 124 | "between": between, 125 | "env": env, 126 | "grep": grep, 127 | "include": include, 128 | "split": split, 129 | } 130 | 131 | // Parse the file 132 | t := template.Must(template.New("t1").Funcs(funcMap).Parse(string(content))) 133 | 134 | // Render 135 | err = t.Execute(os.Stdout, nil) 136 | 137 | return err 138 | } 139 | -------------------------------------------------------------------------------- /cmd_env_template.tmpl: -------------------------------------------------------------------------------- 1 | Hello {{env "USER"}}! 2 | 3 | Your $PATH variable has {{len (split (env "PATH") ":")}} entries. 4 | 5 | The contents of $PATH, one per line: 6 | {{$arr := split (env "PATH") ":"}} 7 | {{range $k, $v := $arr}}* {{$k}} {{$v}} 8 | {{end}} 9 | 10 | Contents of /etc/issue: 11 | 12 | {{include "/etc/issue"}} 13 | 14 | Our present working directory: 15 | 16 | {{include "|pwd"}} 17 | 18 | Files which contain env-template stuff: 19 | 20 | {{grep "|/bin/ls -1" "_env_"}} 21 | -------------------------------------------------------------------------------- /cmd_env_template_helpers.go: -------------------------------------------------------------------------------- 1 | // helper functions for template-expansion. 2 | 3 | package main 4 | 5 | import ( 6 | "fmt" 7 | "os" 8 | "os/exec" 9 | "regexp" 10 | "strings" 11 | ) 12 | 13 | // between finds the text between two regular expressions, from either 14 | // a file or the output of a given command. 15 | func between(in string, begin string, end string) string { 16 | 17 | var content []byte 18 | var err error 19 | 20 | // Read the named file/command-output here. 21 | if strings.HasPrefix(in, "|") { 22 | content, err = runCommand(strings.TrimPrefix(in, "|")) 23 | } else { 24 | content, err = os.ReadFile(in) 25 | } 26 | 27 | if err != nil { 28 | return fmt.Sprintf("error reading %s: %s", in, err.Error()) 29 | } 30 | 31 | // temporary holder 32 | res := []string{} 33 | 34 | // found the open? 35 | var found bool 36 | 37 | // for each line 38 | for _, line := range strings.Split(string(content), "\n") { 39 | 40 | // in the section we care about? 41 | var matched bool 42 | matched, err = regexp.MatchString(begin, line) 43 | if err != nil { 44 | return fmt.Sprintf("error matching %s: %s", begin, err.Error()) 45 | } 46 | 47 | // if we matched add the line 48 | if matched || found { 49 | res = append(res, line) 50 | } 51 | 52 | // if we matched, or we're in a match 53 | // then skip 54 | if matched { 55 | found = true 56 | continue 57 | } 58 | 59 | // are we closing a match? 60 | if found { 61 | matched, err = regexp.MatchString(end, line) 62 | if err != nil { 63 | return fmt.Sprintf("error matching %s: %s", end, err.Error()) 64 | } 65 | 66 | if matched { 67 | found = false 68 | } 69 | } 70 | } 71 | return strings.Join(res, "\n") 72 | } 73 | 74 | // env returns the contents of an environmental variable. 75 | func env(s string) string { 76 | return (os.Getenv(s)) 77 | } 78 | 79 | // grep allows filtering the contents of a file, or output of a command, 80 | // via a regular expression. 81 | func grep(in string, pattern string) string { 82 | var content []byte 83 | var err error 84 | 85 | // Read the named file/command-output here. 86 | if strings.HasPrefix(in, "|") { 87 | content, err = runCommand(strings.TrimPrefix(in, "|")) 88 | } else { 89 | content, err = os.ReadFile(in) 90 | } 91 | 92 | if err != nil { 93 | return fmt.Sprintf("error reading %s: %s", in, err.Error()) 94 | } 95 | 96 | var matched bool 97 | res := []string{} 98 | for _, line := range strings.Split(string(content), "\n") { 99 | matched, err = regexp.MatchString(pattern, line) 100 | if err != nil { 101 | return fmt.Sprintf("error matching %s: %s", pattern, err.Error()) 102 | } 103 | if matched { 104 | res = append(res, line) 105 | } 106 | } 107 | return strings.Join(res, "\n") 108 | 109 | } 110 | 111 | // include inserts the contents of a file, or output of a command 112 | func include(in string) string { 113 | 114 | var content []byte 115 | var err error 116 | 117 | // Read the named file/command-output here. 118 | if strings.HasPrefix(in, "|") { 119 | content, err = runCommand(strings.TrimPrefix(in, "|")) 120 | } else { 121 | content, err = os.ReadFile(in) 122 | } 123 | 124 | if err != nil { 125 | return fmt.Sprintf("error reading %s: %s", in, err.Error()) 126 | } 127 | return (string(content)) 128 | } 129 | 130 | // runCommand returns the output of running the given command 131 | func runCommand(command string) ([]byte, error) { 132 | 133 | // Build up the thing to run, using a shell so that 134 | // we can handle pipes/redirection. 135 | toRun := []string{"/bin/bash", "-c", command} 136 | 137 | // Run the command 138 | cmd := exec.Command(toRun[0], toRun[1:]...) 139 | 140 | // Get the output 141 | output, err := cmd.CombinedOutput() 142 | if err != nil { 143 | return []byte{}, fmt.Errorf("error running command '%s' %s", command, err.Error()) 144 | } 145 | 146 | // Strip trailing newline. 147 | return output, nil 148 | } 149 | 150 | // split converts a string to an array. 151 | func split(in string, delim string) []string { 152 | return strings.Split(in, delim) 153 | } 154 | -------------------------------------------------------------------------------- /cmd_exec_stdin.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "flag" 6 | "fmt" 7 | "os" 8 | "os/exec" 9 | "strings" 10 | 11 | "github.com/skx/sysbox/templatedcmd" 12 | ) 13 | 14 | // Structure for our options and state. 15 | type execSTDINCommand struct { 16 | 17 | // testing the command 18 | dryRun bool 19 | 20 | // parallel job count 21 | parallel int 22 | 23 | // verbose flag 24 | verbose bool 25 | 26 | // field separator 27 | split string 28 | } 29 | 30 | // Command holds a command we're going to execute in a worker-process. 31 | // 32 | // (Command in this sense is a system-binary / external process.) 33 | type Command struct { 34 | 35 | // args holds the command + args to execute. 36 | args []string 37 | } 38 | 39 | // Arguments adds per-command args to the object. 40 | func (es *execSTDINCommand) Arguments(f *flag.FlagSet) { 41 | f.BoolVar(&es.dryRun, "dry-run", false, "Don't run the command.") 42 | f.BoolVar(&es.verbose, "verbose", false, "Be verbose.") 43 | f.IntVar(&es.parallel, "parallel", 1, "How many jobs to run in parallel.") 44 | f.StringVar(&es.split, "split", "", "Split on a different character.") 45 | 46 | } 47 | 48 | // worker reads a command to execute from the channel, and executes it. 49 | // 50 | // The result is pushed back, but ignored. 51 | func (es *execSTDINCommand) worker(id int, jobs <-chan Command, results chan<- int) { 52 | for j := range jobs { 53 | 54 | // Run the command, and get the output? 55 | cmd := exec.Command(j.args[0], j.args[1:]...) 56 | out, errr := cmd.CombinedOutput() 57 | 58 | // error? 59 | if errr != nil { 60 | fmt.Printf("Error running '%s': %s\n", strings.Join(j.args, " "), errr.Error()) 61 | } else { 62 | 63 | // Show the output 64 | fmt.Printf("%s", out) 65 | } 66 | 67 | // Send a result to our output channel. 68 | results <- 1 69 | } 70 | } 71 | 72 | // Info returns the name of this subcommand. 73 | func (es *execSTDINCommand) Info() (string, string) { 74 | return "exec-stdin", `Execute a command for each line of STDIN. 75 | 76 | Details: 77 | 78 | This command reads lines from STDIN, and executes the specified command with 79 | that line as input. 80 | 81 | The line read from STDIN will be available as '{}' and each space-separated 82 | field will be available as {1}, {2}, etc. 83 | 84 | Examples: 85 | 86 | $ echo -e "foo\tbar\nbar\tSteve" | sysbox exec-stdin echo {1} 87 | foo 88 | bar 89 | 90 | Here you see that STDIN would contain: 91 | 92 | foo bar 93 | bar Steve 94 | 95 | However only the first field was displayed, because {1} means the first field. 96 | 97 | To show all input you'd run: 98 | 99 | $ echo -e "foo\tbar\nbar\tSteve" | sysbox exec-stdin echo {} 100 | foo bar 101 | bar Steve 102 | 103 | Flags: 104 | 105 | If you prefer you can split fields on specific characters, which is useful 106 | for operating upon CSV files, or in case you wish to split '/etc/passwd' on 107 | ':' to work on usernames: 108 | 109 | $ cat /etc/passwd | sysbox exec-stdin -split=: groups {1} 110 | 111 | If you wish you can run the commands in parallel, using the -parallel flag 112 | to denote how many simultaneous executions are permitted. 113 | 114 | The only other flag is '-verbose', to show the command that would be 115 | executed and '-dry-run' to avoid running anything.` 116 | } 117 | 118 | // Execute is invoked if the user specifies `exec-stdin` as the subcommand. 119 | func (es *execSTDINCommand) Execute(args []string) int { 120 | 121 | // 122 | // Join all arguments, in case we have been given "{1}", "{2}", etc. 123 | // 124 | cmd := "" 125 | 126 | for _, arg := range args { 127 | cmd += arg 128 | cmd += " " 129 | } 130 | 131 | // 132 | // Ensure we have a command. 133 | // 134 | if cmd == "" { 135 | fmt.Printf("Usage: sysbox exec-stdin command .. args {}..\n") 136 | return 1 137 | } 138 | 139 | // 140 | // Prepare to read line-by-line 141 | // 142 | scanner := bufio.NewReader(os.Stdin) 143 | 144 | // 145 | // The jobs we're going to add. 146 | // 147 | // We save these away so that we can allow parallel execution later. 148 | // 149 | var toRun []Command 150 | 151 | // 152 | // Read a line 153 | // 154 | line, err := scanner.ReadString(byte('\n')) 155 | for err == nil && line != "" { 156 | 157 | // 158 | // Create the command to execute 159 | // 160 | run := templatedcmd.Expand(cmd, line, es.split) 161 | 162 | // 163 | // Show command if being verbose 164 | // 165 | if es.verbose || es.dryRun { 166 | fmt.Printf("%s\n", strings.Join(run, " ")) 167 | } 168 | 169 | // 170 | // If we're not in "pretend"-mode then we'll save the 171 | // constructed command away. 172 | // 173 | if !es.dryRun { 174 | toRun = append(toRun, Command{args: run}) 175 | } 176 | 177 | // 178 | // Loop again 179 | // 180 | line, err = scanner.ReadString(byte('\n')) 181 | } 182 | 183 | // 184 | // We've built up all the commands we're going to run now. 185 | // 186 | // Get the number, and create suitable channels. 187 | // 188 | num := len(toRun) 189 | jobs := make(chan Command, num) 190 | results := make(chan int, num) 191 | 192 | // 193 | // Launch the appropriate number of parallel workers. 194 | // 195 | for w := 1; w <= es.parallel; w++ { 196 | go es.worker(w, jobs, results) 197 | } 198 | 199 | // 200 | // Add all the pending jobs. 201 | // 202 | for _, j := range toRun { 203 | jobs <- j 204 | } 205 | close(jobs) 206 | 207 | // 208 | // Await all the results. 209 | // 210 | for a := 1; a <= num; a++ { 211 | <-results 212 | } 213 | return 0 214 | } 215 | -------------------------------------------------------------------------------- /cmd_expect.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "context" 6 | "encoding/csv" 7 | "fmt" 8 | "os" 9 | "os/exec" 10 | "strconv" 11 | "strings" 12 | "syscall" 13 | "time" 14 | 15 | "github.com/skx/subcommands" 16 | 17 | expect "github.com/google/goexpect" 18 | ) 19 | 20 | // Structure for our options and state. 21 | type expectCommand struct { 22 | 23 | // Timeout for running our command/waiting. 24 | // 25 | // This is set via the script-file, rather than a command-line argument. 26 | timeout time.Duration 27 | 28 | // We embed the NoFlags option, because we accept no command-line flags. 29 | subcommands.NoFlags 30 | } 31 | 32 | // Info returns the name of this subcommand. 33 | func (ec *expectCommand) Info() (string, string) { 34 | return "expect", `A simple utility for scripting interactive commands. 35 | 36 | Details: 37 | 38 | This command allows you to execute an arbitrary process, sending input for 39 | matching output which is received. It is a simple alternative to the 'expect' 40 | utility, famously provided with TCL. 41 | 42 | The command requires a configuration file to be specified which contains details 43 | of the process to be executed, and the output/input to receive/send. 44 | 45 | Here is a simple example, note that the output the command produces is matched via regular expressions, rather than literally. That's why you'll see "\." used to match a literal period: 46 | 47 | # Comments are prefixed with '#' 48 | # Timeout is expressed in seconds 49 | TIMEOUT 10 50 | 51 | # The command to run 52 | SPAWN telnet telehack.com 53 | 54 | # Now the dialog 55 | EXPECT \n\. 56 | SEND date\r\n 57 | EXPECT \n\. 58 | SEND quit\r\n 59 | 60 | You'll see we use '\r\n' because we're using telnet, for driving bash and other normal commands you'd use '\n' instead as you would expect: 61 | 62 | TIMEOUT 10 63 | SPAWN /bin/bash --login 64 | EXPECT $ 65 | SEND touch /tmp/meow\n 66 | EXPECT $ 67 | SEND exit\n 68 | 69 | If you wish to execute a command, or arguments, containing spaces that is supported via quoting: 70 | 71 | SPAWN /path/to/foo arg1 "argument two" arg3 .. 72 | ` 73 | } 74 | 75 | // Run a command, and return something suitable for matching against with 76 | // the expect library we're using.. 77 | func (ec *expectCommand) expectExec(cmd []string) (*expect.GExpect, func() error, error) { 78 | 79 | c := exec.CommandContext( 80 | context.Background(), 81 | cmd[0], cmd[1:]...) 82 | 83 | // write error out to my stdout 84 | c.Stderr = os.Stderr 85 | 86 | stdIn, err := c.StdinPipe() 87 | if err != nil { 88 | return nil, nil, fmt.Errorf("error creating pipe: %s", err) 89 | } 90 | 91 | stdOut, err := c.StdoutPipe() 92 | if err != nil { 93 | return nil, nil, fmt.Errorf("error creating pipe: %s", err) 94 | } 95 | 96 | if err = c.Start(); err != nil { 97 | return nil, nil, fmt.Errorf("unexpected error starting command: %+v", err) 98 | } 99 | 100 | waitCh := make(chan error, 1) 101 | 102 | e, _, err := expect.SpawnGeneric( 103 | &expect.GenOptions{ 104 | In: stdIn, 105 | Out: stdOut, 106 | Wait: func() error { 107 | er := c.Wait() 108 | waitCh <- er 109 | return err 110 | }, 111 | Close: c.Process.Kill, 112 | Check: func() bool { 113 | if c.Process == nil { 114 | return false 115 | } 116 | return c.Process.Signal(syscall.Signal(0)) == nil 117 | }, 118 | }, 119 | ec.timeout, 120 | expect.Verbose(true), 121 | expect.VerboseWriter(os.Stdout), 122 | ) 123 | if err != nil { 124 | return nil, nil, fmt.Errorf("error creating expect: %s", err) 125 | } 126 | 127 | wait := func() error { 128 | err := <-waitCh 129 | return err 130 | } 131 | 132 | return e, wait, nil 133 | } 134 | 135 | // Execute is invoked if the user specifies `expect` as the subcommand. 136 | func (ec *expectCommand) Execute(args []string) int { 137 | 138 | // Ensure we have a config-file 139 | if len(args) <= 0 { 140 | fmt.Printf("Usage: expect /path/to/config.script\n") 141 | return 1 142 | } 143 | 144 | // We'll now open the configuration file 145 | handle, err := os.Open(args[0]) 146 | if err != nil { 147 | fmt.Printf("error opening %s : %s\n", args[0], err.Error()) 148 | return 1 149 | } 150 | 151 | // Timeout Value 152 | ec.timeout = 60 * time.Second 153 | 154 | // Command 155 | cmd := "/bin/sh" 156 | 157 | // Read/Send stuff 158 | interaction := []expect.Batcher{} 159 | 160 | // Allow reading line by line 161 | reader := bufio.NewReader(handle) 162 | 163 | line, err := reader.ReadString(byte('\n')) 164 | for err == nil { 165 | 166 | // Lose the space 167 | line = strings.TrimSpace(line) 168 | 169 | // Timeout? 170 | if strings.HasPrefix(line, "TIMEOUT ") { 171 | 172 | line = strings.TrimPrefix(line, "TIMEOUT ") 173 | line = strings.TrimSpace(line) 174 | 175 | val, er := strconv.Atoi(line) 176 | if er != nil { 177 | fmt.Printf("error converting timeout value %s to number: %s\n", line, er) 178 | return 1 179 | } 180 | 181 | ec.timeout = time.Duration(val) * time.Second 182 | } 183 | 184 | // Command 185 | if strings.HasPrefix(line, "SPAWN ") { 186 | cmd = strings.TrimPrefix(line, "SPAWN ") 187 | cmd = strings.TrimSpace(cmd) 188 | } 189 | 190 | // Expect 191 | if strings.HasPrefix(line, "EXPECT ") { 192 | 193 | line = strings.TrimPrefix(line, "EXPECT ") 194 | line = strings.TrimSpace(line) 195 | line = strings.ReplaceAll(line, "\\n", "\n") 196 | line = strings.ReplaceAll(line, "\\r", "\r") 197 | line = strings.ReplaceAll(line, "\\t", "\t") 198 | interaction = append(interaction, &expect.BExp{R: line}) 199 | } 200 | 201 | // Send 202 | if strings.HasPrefix(line, "SEND ") { 203 | line = strings.TrimPrefix(line, "SEND ") 204 | line = strings.TrimSpace(line) 205 | line = strings.ReplaceAll(line, "\\n", "\n") 206 | line = strings.ReplaceAll(line, "\\r", "\r") 207 | line = strings.ReplaceAll(line, "\\t", "\t") 208 | interaction = append(interaction, &expect.BSnd{S: line}) 209 | } 210 | 211 | // Loop again 212 | line, err = reader.ReadString(byte('\n')) 213 | } 214 | 215 | // Launch the command 216 | fmt.Printf("Running: '%s'\n", cmd) 217 | 218 | // Split the command into fields, taking into account quoted strings. 219 | // 220 | // So the user can run things like this: 221 | // echo "foo bar" 3 222 | // 223 | // https://stackoverflow.com/questions/47489745/ 224 | // 225 | r := csv.NewReader(strings.NewReader(cmd)) 226 | r.Comma = ' ' 227 | record, err := r.Read() 228 | if err != nil { 229 | fmt.Printf("failed to split %s : %s\n", cmd, err) 230 | return 1 231 | } 232 | 233 | // Launch the command using the record array we've just parsed. 234 | e, wait, err := ec.expectExec(record) 235 | if err != nil { 236 | fmt.Printf("error launching %s: %s\n", cmd, err) 237 | return 1 238 | } 239 | defer e.Close() 240 | 241 | // Wire up the expect-magic. 242 | _, err = e.ExpectBatch(interaction, ec.timeout) 243 | if err != nil { 244 | fmt.Printf("error running recipe:%s\n", err) 245 | return 1 246 | } 247 | 248 | // Now await completion of the command/process. 249 | if err := wait(); err != nil { 250 | fmt.Printf("error waiting for process: %s\n", err) 251 | return 1 252 | } 253 | 254 | return 0 255 | } 256 | -------------------------------------------------------------------------------- /cmd_feeds.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "io" 7 | "net/http" 8 | "net/url" 9 | "strings" 10 | 11 | "github.com/skx/subcommands" 12 | "golang.org/x/net/html" 13 | ) 14 | 15 | // Structure for our options and state. 16 | type feedsCommand struct { 17 | 18 | // We embed the NoFlags option, because we accept no command-line flags. 19 | subcommands.NoFlags 20 | } 21 | 22 | // ErrNoFeeds is used if no feeds are found in a remote URL 23 | var ErrNoFeeds = errors.New("NO-FEED") 24 | 25 | // Info returns the name of this subcommand. 26 | func (t *feedsCommand) Info() (string, string) { 27 | return "feeds", `Extract RSS feeds from remote URLS. 28 | 29 | Details: 30 | 31 | This command fetches the contents of the specified URL, much like 32 | the 'http-get' command would, and extracts any specified RSS feed 33 | from the contents of that remote URL. 34 | 35 | Examples: 36 | 37 | $ sysbox feeds https://blog.steve.fi/` 38 | } 39 | 40 | func (t *feedsCommand) FindFeeds(base string) ([]string, error) { 41 | 42 | ret := []string{} 43 | 44 | if !strings.HasPrefix(base, "http") { 45 | base = "https://" + base 46 | } 47 | 48 | // Make the request 49 | response, err := http.Get(base) 50 | if err != nil { 51 | return ret, err 52 | } 53 | 54 | // Get the body. 55 | defer response.Body.Close() 56 | 57 | // Create a parser 58 | z := html.NewTokenizer(response.Body) 59 | 60 | // Use the parser to get the links 61 | ret, err = t.runparser(z, base) 62 | 63 | // Error? Return that. 64 | if err != nil { 65 | return ret, err 66 | } 67 | 68 | // No feed-links? Then return the error-sentinel. 69 | if len(ret) == 0 { 70 | return ret, ErrNoFeeds 71 | } 72 | 73 | // All good 74 | return ret, nil 75 | } 76 | 77 | // runparser uses the given parser to look for feeds, and returns those it fouind 78 | func (t *feedsCommand) runparser(z *html.Tokenizer, base string) ([]string, error) { 79 | 80 | ret := []string{} 81 | 82 | for { 83 | tt := z.Next() 84 | switch tt { 85 | case html.ErrorToken: 86 | err := z.Err() 87 | if err == io.EOF { 88 | if len(ret) > 0 { 89 | return ret, nil 90 | } 91 | return ret, ErrNoFeeds 92 | } 93 | return ret, fmt.Errorf("%s", z.Err()) 94 | case html.StartTagToken, html.SelfClosingTagToken: 95 | t := z.Token() 96 | if t.Data == "link" { 97 | isRSS := false 98 | u := "" 99 | for _, attr := range t.Attr { 100 | if attr.Key == "type" && (attr.Val == "application/rss+xml" || attr.Val == "application/atom+xml") { 101 | isRSS = true 102 | } 103 | 104 | if attr.Key == "href" { 105 | u = attr.Val 106 | } 107 | } 108 | if isRSS { 109 | if !strings.HasPrefix(u, "http") { 110 | u, _ = url.JoinPath(base, u) 111 | } 112 | ret = append(ret, u) 113 | } 114 | } 115 | } 116 | } 117 | 118 | } 119 | 120 | // Execute is invoked if the user specifies `feeds` as the subcommand. 121 | func (t *feedsCommand) Execute(args []string) int { 122 | 123 | // Ensure we have only a single URL 124 | if len(args) != 1 { 125 | fmt.Printf("Usage: feeds URL\n") 126 | return 1 127 | } 128 | 129 | // The URL 130 | url := args[0] 131 | 132 | // We'll default to https if the protocol isn't specified. 133 | if !strings.HasPrefix(url, "http") { 134 | url = "https://" + url 135 | } 136 | 137 | out, err := t.FindFeeds(url) 138 | if err != nil { 139 | if err == ErrNoFeeds { 140 | fmt.Printf("No Feeds found in %s\n", url) 141 | } else { 142 | fmt.Printf("Error processing %s: %s\n", url, err) 143 | return 1 144 | } 145 | } else { 146 | for _, x := range out { 147 | fmt.Printf("%s\n", x) 148 | } 149 | } 150 | 151 | return 0 152 | } 153 | -------------------------------------------------------------------------------- /cmd_find.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "os" 7 | "path/filepath" 8 | "regexp" 9 | ) 10 | 11 | // Structure for our options and state. 12 | type findCommand struct { 13 | 14 | // Show the names of matching directories? 15 | directories bool 16 | 17 | // Show the names of matching files? 18 | files bool 19 | 20 | // Starting path 21 | path string 22 | 23 | // Ignore errors? 24 | silent bool 25 | } 26 | 27 | // Info returns the name of this subcommand. 28 | func (fc *findCommand) Info() (string, string) { 29 | return "find", `Trivial file-finder. 30 | 31 | Details: 32 | 33 | This command is a minimal 'find' replacement, allowing you to find 34 | files by regular expression. Basic usage defaults to finding files, 35 | by regular expression: 36 | 37 | $ sysbox find 'foo*.go' '_test' 38 | 39 | To find directories instead of files: 40 | 41 | $ sysbox find -files=false -directories=true 'blah$' 42 | 43 | Or both: 44 | 45 | $ sysbox find -path=/etc -files=true -directories=true 'blah$' 46 | ` 47 | } 48 | 49 | // Arguments adds per-command args to the object. 50 | func (fc *findCommand) Arguments(f *flag.FlagSet) { 51 | f.BoolVar(&fc.files, "files", true, "Show the names of matching files?") 52 | f.BoolVar(&fc.directories, "directories", false, "Show the names of matching directories?") 53 | f.BoolVar(&fc.silent, "silent", true, "Ignore permission-denied errors when recursing into unreadable directories?") 54 | f.StringVar(&fc.path, "path", ".", "Starting path for search.") 55 | } 56 | 57 | // find runs the find operation 58 | func (fc *findCommand) find(patterns []string) error { 59 | 60 | // build up a list of regular expressions 61 | regs := []*regexp.Regexp{} 62 | 63 | for _, pattern := range patterns { 64 | 65 | reg, err := regexp.Compile(pattern) 66 | if err != nil { 67 | return fmt.Errorf("failed to compile %s:%s", pattern, err) 68 | } 69 | 70 | regs = append(regs, reg) 71 | } 72 | 73 | // 74 | // Walk the filesystem 75 | // 76 | err := filepath.Walk(fc.path, 77 | func(path string, info os.FileInfo, err error) error { 78 | if err != nil { 79 | if !os.IsPermission(err) { 80 | return err 81 | } 82 | 83 | if !fc.silent { 84 | fmt.Fprintln(os.Stderr, "permission denied handling "+path) 85 | } 86 | } 87 | 88 | // We have a path. 89 | // 90 | // If it doesn't match any of our regexps then we return. 91 | // 92 | // i.e. We must match ALL supplied patterns, not just 93 | // one of them. 94 | // 95 | for _, r := range regs { 96 | if !r.MatchString(path) { 97 | return nil 98 | } 99 | } 100 | 101 | // is it a file? 102 | isDir := info.IsDir() 103 | isFile := !isDir 104 | 105 | if (isDir && fc.directories) || 106 | (isFile && fc.files) { 107 | fmt.Printf("%s\n", path) 108 | } 109 | return nil 110 | }) 111 | 112 | if err != nil { 113 | return fmt.Errorf("error walking filesystem %s", err) 114 | } 115 | 116 | return nil 117 | } 118 | 119 | // Execute is invoked if the user specifies `find` as the subcommand. 120 | func (fc *findCommand) Execute(args []string) int { 121 | 122 | // 123 | // Build up the list of patterns 124 | // 125 | patterns := []string{} 126 | patterns = append(patterns, args...) 127 | 128 | // 129 | // Ensure we have a least one. 130 | // 131 | if len(patterns) < 1 { 132 | fmt.Printf("Usage: sysbox find pattern1 [pattern2..]\n") 133 | return 1 134 | } 135 | 136 | // 137 | // Run the find. 138 | // 139 | err := fc.find(patterns) 140 | if err != nil { 141 | fmt.Printf("%s\n", err) 142 | return 1 143 | } 144 | 145 | // 146 | // All done 147 | // 148 | return 0 149 | } 150 | -------------------------------------------------------------------------------- /cmd_fingerd.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "errors" 6 | "flag" 7 | "fmt" 8 | "net" 9 | "os" 10 | "os/user" 11 | "strconv" 12 | ) 13 | 14 | // Structure for our options and state. 15 | type fingerdCommand struct { 16 | port int 17 | } 18 | 19 | // Arguments adds per-command args to the object. 20 | func (fc *fingerdCommand) Arguments(f *flag.FlagSet) { 21 | f.IntVar(&fc.port, "port", 79, "The port to listen upon") 22 | } 23 | 24 | // Info returns the name of this subcommand. 25 | func (fc *fingerdCommand) Info() (string, string) { 26 | return "fingerd", `A small finger daemon. 27 | 28 | Details: 29 | 30 | This command provides a simple finger server, which allows remote users 31 | to finger your local users. 32 | 33 | The file ~/.plan will be served to any remote clients who inspect your 34 | users. 35 | 36 | Examples: 37 | 38 | $ sysbox fingerd & 39 | $ echo "I like cakes" > ~/.plan 40 | $ finger $USER@localhost 41 | 42 | Security: 43 | 44 | To allow this to be started as a non-root user you'll want to 45 | run something like: 46 | 47 | $ sudo setcap cap_net_bind_service=+ep /path/to/sysbox 48 | 49 | This is better than dropping privileges and starting as root 50 | as a result of the lack of reliability of the latter. See 51 | https://github.com/golang/go/issues/1435 for details 52 | 53 | The alternative would be to bind to :7979 and use iptables 54 | to redirect access from :79 -> 127.0.0.1:7979. 55 | 56 | Something like this for external access: 57 | 58 | # iptables -t nat -A PREROUTING -p tcp -m tcp --dport 79 -j REDIRECT --to-ports 7979 59 | 60 | And finally for localhost access: 61 | 62 | # iptables -t nat -A OUTPUT -o lo -p tcp --dport 79 -j REDIRECT --to-port 7979 63 | ` 64 | } 65 | 66 | // Execute is invoked if the user specifies `fingerd` as the subcommand. 67 | func (fc *fingerdCommand) Execute(args []string) int { 68 | 69 | // Listen 70 | ln, err := net.Listen("tcp", ":"+strconv.Itoa(fc.port)) 71 | if err != nil { 72 | fmt.Printf("failed to bind to port %d:n%s\n", 73 | fc.port, err.Error()) 74 | return 1 75 | } 76 | 77 | // Accept 78 | for { 79 | conn, err := ln.Accept() 80 | if err != nil { 81 | continue 82 | } 83 | go fc.handleConnection(conn) 84 | } 85 | } 86 | 87 | func (fc *fingerdCommand) handleConnection(conn net.Conn) { 88 | defer conn.Close() 89 | reader := bufio.NewReader(conn) 90 | usr, _, _ := reader.ReadLine() 91 | 92 | info, err := fc.getUserInfo(string(usr)) 93 | if err != nil { 94 | conn.Write([]byte(err.Error())) 95 | } else { 96 | conn.Write(info) 97 | } 98 | } 99 | 100 | func (fc *fingerdCommand) getUserInfo(usr string) ([]byte, error) { 101 | u, e := user.Lookup(usr) 102 | if e != nil { 103 | return nil, e 104 | } 105 | data, err := os.ReadFile(u.HomeDir + "/.plan") 106 | if err != nil { 107 | return data, errors.New("user doesn't have a .plan file") 108 | } 109 | return data, nil 110 | } 111 | -------------------------------------------------------------------------------- /cmd_html2text.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "fmt" 6 | "os" 7 | "strings" 8 | 9 | "github.com/skx/subcommands" 10 | "golang.org/x/net/html" 11 | ) 12 | 13 | // Structure for our options and state. 14 | type html2TextCommand struct { 15 | 16 | // We embed the NoFlags option, because we accept no command-line flags. 17 | subcommands.NoFlags 18 | } 19 | 20 | // Info returns the name of this subcommand. 21 | func (h2t *html2TextCommand) Info() (string, string) { 22 | return "html2text", `HTML to text conversion. 23 | 24 | This command converts the contents of STDIN, or the named files, 25 | from HTML to text, and prints them to the console. 26 | 27 | Examples: 28 | 29 | $ curl --silent https://steve.fi/ | sysbox html2text | less 30 | $ sysbox html2text /usr/share/doc/gdisk/gdisk.html |less 31 | 32 | 33 | ` 34 | } 35 | 36 | func (h2t *html2TextCommand) process(reader *bufio.Reader) { 37 | 38 | domDocTest := html.NewTokenizer(reader) 39 | previousStartTokenTest := domDocTest.Token() 40 | loopDomTest: 41 | 42 | for { 43 | tt := domDocTest.Next() 44 | switch { 45 | case tt == html.ErrorToken: 46 | break loopDomTest // End of the document, done 47 | case tt == html.StartTagToken: 48 | previousStartTokenTest = domDocTest.Token() 49 | case tt == html.TextToken: 50 | if previousStartTokenTest.Data == "script" || 51 | previousStartTokenTest.Data == "style" { 52 | continue 53 | } 54 | TxtContent := strings.TrimSpace(html.UnescapeString(string(domDocTest.Text()))) 55 | if len(TxtContent) > 0 { 56 | fmt.Printf("%s\n", TxtContent) 57 | } 58 | } 59 | } 60 | } 61 | 62 | // Execute is invoked if the user specifies `html2text` as the subcommand. 63 | func (h2t *html2TextCommand) Execute(args []string) int { 64 | 65 | // 66 | // Read from STDIN 67 | // 68 | if len(args) == 0 { 69 | 70 | scanner := bufio.NewReader(os.Stdin) 71 | 72 | h2t.process(scanner) 73 | 74 | return 0 75 | } 76 | 77 | // 78 | // Otherwise each named file 79 | // 80 | for _, file := range args { 81 | 82 | handle, err := os.Open(file) 83 | if err != nil { 84 | fmt.Printf("error opening %s : %s\n", file, err.Error()) 85 | return 1 86 | } 87 | 88 | reader := bufio.NewReader(handle) 89 | h2t.process(reader) 90 | } 91 | return 0 92 | } 93 | -------------------------------------------------------------------------------- /cmd_http_get.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "io" 7 | "net/http" 8 | "sort" 9 | "strings" 10 | ) 11 | 12 | // Structure for our options and state. 13 | type httpGetCommand struct { 14 | 15 | // Show headers? 16 | headers bool 17 | 18 | // Show body? 19 | body bool 20 | } 21 | 22 | // Arguments adds per-command args to the object. 23 | func (hg *httpGetCommand) Arguments(f *flag.FlagSet) { 24 | f.BoolVar(&hg.body, "body", true, "Show the response body.") 25 | f.BoolVar(&hg.headers, "headers", false, "Show the response headers.") 26 | 27 | } 28 | 29 | // Info returns the name of this subcommand. 30 | func (hg *httpGetCommand) Info() (string, string) { 31 | return "http-get", `Download and display the contents of a remote URL. 32 | 33 | Details: 34 | 35 | This command is very much curl-lite, allowing you to fetch the contents of 36 | a remote URL, with no configuration options of any kind. 37 | 38 | While it is unusual to find hosts without curl or wget installed it does 39 | happen, this command will bridge the gap a little. 40 | 41 | Examples: 42 | 43 | $ sysbox http-get https://steve.fi/` 44 | } 45 | 46 | // Execute is invoked if the user specifies `http-get` as the subcommand. 47 | func (hg *httpGetCommand) Execute(args []string) int { 48 | 49 | // Ensure we have only a single URL 50 | if len(args) != 1 { 51 | fmt.Printf("Usage: http-get URL\n") 52 | return 1 53 | } 54 | 55 | // The URL 56 | url := args[0] 57 | 58 | // We'll default to https if the protocol isn't specified. 59 | if !strings.HasPrefix(url, "http") { 60 | url = "https://" + url 61 | } 62 | 63 | // Make the request 64 | response, err := http.Get(url) 65 | if err != nil { 66 | fmt.Printf("error fetching %s: %s", url, err.Error()) 67 | return 1 68 | } 69 | 70 | // Get the body. 71 | defer response.Body.Close() 72 | contents, err := io.ReadAll(response.Body) 73 | if err != nil { 74 | fmt.Printf("error: %s", err.Error()) 75 | return 1 76 | } 77 | 78 | // Show header? 79 | if hg.headers { 80 | 81 | // Keep a list of the headers here for sort/display 82 | headers := []string{} 83 | 84 | // Copy the headers 85 | for header := range response.Header { 86 | headers = append(headers, header) 87 | } 88 | 89 | // Sort them 90 | sort.Strings(headers) 91 | 92 | // Output them 93 | for _, header := range headers { 94 | fmt.Printf("%s: %s\n", header, response.Header.Get(header)) 95 | } 96 | } 97 | 98 | // If showing header and body separate them both 99 | if hg.headers && hg.body { 100 | fmt.Printf("\n") 101 | } 102 | 103 | // Show body? 104 | if hg.body { 105 | fmt.Printf("%s\n", string(contents)) 106 | } 107 | 108 | return 0 109 | } 110 | -------------------------------------------------------------------------------- /cmd_httpd.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "log" 7 | "net/http" 8 | ) 9 | 10 | // Structure for our options and state. 11 | type httpdCommand struct { 12 | host string 13 | port int 14 | path string 15 | } 16 | 17 | // Arguments adds per-command args to the object. 18 | func (h *httpdCommand) Arguments(f *flag.FlagSet) { 19 | 20 | f.StringVar(&h.path, "path", ".", "The directory to use as the HTTP root directory") 21 | f.StringVar(&h.host, "host", "127.0.0.1", "The host to bind upon (use 0.0.0.0 for remote access)") 22 | f.IntVar(&h.port, "port", 3000, "The port to listen upon") 23 | 24 | } 25 | 26 | // Info returns the name of this subcommand. 27 | func (h *httpdCommand) Info() (string, string) { 28 | return "httpd", `A simple HTTP server. 29 | 30 | Details: 31 | 32 | This command implements a simple HTTP-server, which defaults to serving 33 | the contents found beneath the current working directory. 34 | 35 | By default the content is served to the localhost only, but that can 36 | be changed. 37 | 38 | Examples: 39 | 40 | $ sysbox httpd 41 | 2020/04/01 21:36:27 Serving upon http://127.0.0.1:3000/ 42 | 43 | $ sysbox httpd -host=0.0.0.0 -port 8080 44 | 2020/04/01 21:36:45 Serving upon http://0.0.0.0:8080/` 45 | 46 | } 47 | 48 | // Execute is invoked if the user specifies `httpd` as the subcommand. 49 | func (h *httpdCommand) Execute(args []string) int { 50 | 51 | // 52 | // Create a static-file server, based upon the 53 | // path we're treating as our root-directory. 54 | // 55 | fs := http.FileServer(http.Dir(h.path)) 56 | http.Handle("/", fs) 57 | 58 | // 59 | // Build up the listen address. 60 | // 61 | listen := fmt.Sprintf("%s:%d", h.host, h.port) 62 | 63 | // 64 | // Log our start, and begin serving. 65 | // 66 | log.Printf("Serving upon http://%s/\n", listen) 67 | http.ListenAndServe(listen, logRequest(http.DefaultServeMux)) 68 | return 0 69 | } 70 | 71 | // logRequest dumps the request to the console. 72 | // 73 | // Of course we don't know the return-code, but this is good enough 74 | // for most of my use-cases. 75 | func logRequest(handler http.Handler) http.Handler { 76 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 77 | log.Printf("%s %s %s\n", r.RemoteAddr, r.Method, r.URL) 78 | handler.ServeHTTP(w, r) 79 | }) 80 | } 81 | -------------------------------------------------------------------------------- /cmd_ips.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "net" 7 | "strings" 8 | ) 9 | 10 | // Structure for our options and state. 11 | type ipsCommand struct { 12 | 13 | // show only IPv4 addresses? 14 | ipv4 bool 15 | 16 | // show only IPv6 addresses? 17 | ipv6 bool 18 | 19 | // show local addresses? 20 | local bool 21 | 22 | // show global/remote addresses? 23 | remote bool 24 | 25 | // Cached store of network/netmask to IP-range - IPv4 26 | ip4Ranges map[string]*net.IPNet 27 | 28 | // Cached store of network/netmask to IP-range - IPv6 29 | ip6Ranges map[string]*net.IPNet 30 | } 31 | 32 | // Arguments adds per-command args to the object. 33 | func (i *ipsCommand) Arguments(f *flag.FlagSet) { 34 | f.BoolVar(&i.ipv4, "4", true, "Should we show IPv4 addresses?") 35 | f.BoolVar(&i.ipv6, "6", true, "Should we show IPv6 addresses?") 36 | f.BoolVar(&i.local, "local", true, "Should we show local addresses?") 37 | f.BoolVar(&i.remote, "remote", true, "Should we show global addresses?") 38 | 39 | } 40 | 41 | // Info returns the name of this subcommand. 42 | func (i *ipsCommand) Info() (string, string) { 43 | return "ips", `Show IP address information. 44 | 45 | Details: 46 | 47 | This command allows you to see local/global IP addresses assigned to 48 | the current host. 49 | 50 | By default all IP addresses will be shown, but you can disable protocols 51 | and types of addresses you do not wish to see. 52 | 53 | Examples: 54 | 55 | $ sysbox ips -4=false 56 | ::1 57 | fe80::feaa:14ff:fe32:688 58 | fe80::78e5:95b6:1659:b407 59 | 60 | $ sysbox ips -local=false -4=false 61 | 2a01:4f9:c010:27d8::1 62 | ` 63 | } 64 | 65 | // isLocal is a helper to test if an address is "local" or "remote". 66 | func (i *ipsCommand) isLocal(address *net.IPNet) bool { 67 | 68 | localIP4 := []string{ 69 | "10.0.0.0/8", // RFC1918 70 | "100.64.0.0/10", // RFC 6598 71 | "127.0.0.0/8", // IPv4 loopback 72 | "169.254.0.0/16", // RFC3927 link-local 73 | "172.16.0.0/12", // RFC1918 74 | "192.0.0.0/24", // RFC 5736 75 | "192.0.2.0/24", // RFC 5737 76 | "192.168.0.0/16", // RFC1918 77 | "192.18.0.0/15", // RFC 2544 78 | "192.88.99.0/24", // RFC 3068 79 | "198.51.100.0/24", // 80 | "203.0.113.0/24", // 81 | "224.0.0.0/4", // RFC 3171 82 | "255.255.255.255/32", // RFC 919 Section 7 83 | } 84 | localIP6 := []string{ 85 | "::/128", // RFC 4291: Unspecified Address 86 | "100::/64", // RFC 6666: Discard Address Block 87 | "2001:2::/48", // RFC 5180: Benchmarking 88 | "2001::/23", // RFC 2928: IETF Protocol Assignments 89 | "2001::/32", // RFC 4380: TEREDO 90 | "2001:db8::/32", // RFC 3849: Documentation 91 | "::1/128", // RFC 4291: Loopback Address 92 | "fc00::/7", // RFC 4193: Unique-Local 93 | "fe80::/10", // RFC 4291: Section 2.5.6 Link-Scoped Unicast 94 | "ff00::/8", // RFC 4291: Section 2.7 95 | } 96 | 97 | // Create our maps 98 | if i.ip4Ranges == nil { 99 | i.ip4Ranges = make(map[string]*net.IPNet) 100 | i.ip6Ranges = make(map[string]*net.IPNet) 101 | 102 | // Join our ranges. 103 | tmp := localIP4 104 | tmp = append(tmp, localIP6...) 105 | 106 | // For each network-range. 107 | for _, entry := range tmp { 108 | 109 | // Parse 110 | _, block, _ := net.ParseCIDR(entry) 111 | 112 | // Record in the protocol-specific range 113 | if strings.Contains(entry, ":") { 114 | i.ip6Ranges[entry] = block 115 | } else { 116 | i.ip4Ranges[entry] = block 117 | } 118 | } 119 | } 120 | 121 | // The map we're testing from 122 | testMap := i.ip4Ranges 123 | 124 | // Are we testing an IPv6 address? 125 | if strings.Contains(address.String(), ":") { 126 | testMap = i.ip6Ranges 127 | } 128 | 129 | // Loop over the appropriate map and test for inclusion 130 | for _, block := range testMap { 131 | if block.Contains(address.IP) { 132 | return true 133 | } 134 | } 135 | 136 | // Not found. 137 | return false 138 | } 139 | 140 | // Execute is invoked if the user specifies `ips` as the subcommand. 141 | func (i *ipsCommand) Execute(args []string) int { 142 | 143 | // Get addresses 144 | addrs, err := net.InterfaceAddrs() 145 | if err != nil { 146 | fmt.Printf("Error finding IPs:%s\n", err.Error()) 147 | return 1 148 | } 149 | 150 | // For each one 151 | for _, address := range addrs { 152 | 153 | // cast .. 154 | ipnet, ok := address.(*net.IPNet) 155 | if !ok { 156 | fmt.Printf("Failed to convert %v to IP\n", address) 157 | return 1 158 | } 159 | 160 | // If we're not showing locals, then skip if this is. 161 | if !i.local && i.isLocal(ipnet) { 162 | continue 163 | } 164 | 165 | // If we're not showing globals, then skip if this is 166 | if !i.remote && !i.isLocal(ipnet) { 167 | continue 168 | } 169 | 170 | res := ipnet.IP.String() 171 | 172 | // If we're not showing IPv4 and the address is that 173 | // then skip it 174 | if !i.ipv4 && !strings.Contains(res, ":") { 175 | continue 176 | } 177 | 178 | // If we're not showing IPv6 and the address is that then 179 | // skip it 180 | if !i.ipv6 && strings.Contains(res, ":") { 181 | continue 182 | } 183 | 184 | fmt.Println(res) 185 | } 186 | return 0 187 | } 188 | -------------------------------------------------------------------------------- /cmd_markdown_toc.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "flag" 6 | "fmt" 7 | "os" 8 | "strings" 9 | ) 10 | 11 | // Structure for our options and state. 12 | type markdownTOCCommand struct { 13 | 14 | // Maximum level to include. 15 | max int 16 | } 17 | 18 | // tocItem holds state for a single entry 19 | type tocItem struct { 20 | // Depth of the entry 21 | depth int 22 | 23 | // The content of the header (text). 24 | content string 25 | } 26 | 27 | // String converts the tocItem to a string 28 | func (t tocItem) String() string { 29 | 30 | // Characters dropped from anchors 31 | droppedChars := []string{ 32 | "\"", "'", "`", ".", 33 | "!", ",", "~", "&", 34 | "%", "^", "*", "#", 35 | "/", "\\", 36 | "@", "|", 37 | "(", ")", 38 | "{", "}", 39 | "[", "]", 40 | } 41 | 42 | // link is lowercase 43 | link := strings.ToLower(t.content) 44 | 45 | // Remove the characters 46 | for _, c := range droppedChars { 47 | link = strings.Replace(link, c, "", -1) 48 | } 49 | 50 | // Replace everything else with "-" 51 | link = strings.Replace(link, " ", "-", -1) 52 | link = "#" + link 53 | 54 | return fmt.Sprintf("%v* [%v](%v) \n", 55 | strings.Repeat(" ", 2*(t.depth-1)), 56 | t.content, 57 | link) 58 | } 59 | 60 | // Arguments adds per-command args to the object. 61 | func (m *markdownTOCCommand) Arguments(f *flag.FlagSet) { 62 | f.IntVar(&m.max, "max", 100, "The maximum nesting level to generate.") 63 | 64 | } 65 | 66 | // Info returns the name of this subcommand. 67 | func (m *markdownTOCCommand) Info() (string, string) { 68 | return "markdown-toc", `Create a table-of-contents for a markdown file. 69 | 70 | Details: 71 | 72 | This command allows you to generate a (github-themed) table of contents 73 | for a given markdown file. 74 | 75 | 76 | Usage: 77 | 78 | $ sysbox markdown-toc README.md 79 | $ sysbox markdown-toc < README.md` 80 | } 81 | 82 | // process handles the generation of the TOC from the given reader 83 | func (m *markdownTOCCommand) process(reader *bufio.Reader) error { 84 | 85 | fileScanner := bufio.NewScanner(reader) 86 | 87 | for fileScanner.Scan() { 88 | line := fileScanner.Text() 89 | 90 | headerCount := m.countHashes(line) 91 | 92 | if headerCount >= 1 && headerCount < m.max { 93 | 94 | // Create an item for this header 95 | item := tocItem{ 96 | depth: headerCount, 97 | content: line[headerCount+1:], 98 | } 99 | 100 | // Print it 101 | fmt.Print(item.String()) 102 | } 103 | } 104 | 105 | if err := fileScanner.Err(); err != nil { 106 | return err 107 | } 108 | 109 | return nil 110 | } 111 | 112 | // counts hashes at the beginning of a line 113 | func (m *markdownTOCCommand) countHashes(s string) int { 114 | for i, c := range s { 115 | if c != '#' { 116 | return i 117 | } 118 | } 119 | return len(s) 120 | } 121 | 122 | // Execute is invoked if the user specifies `markdown-toc` as the subcommand. 123 | func (m *markdownTOCCommand) Execute(args []string) int { 124 | 125 | var err error 126 | 127 | // No file? Use STDIN 128 | if len(args) == 0 { 129 | 130 | scanner := bufio.NewReader(os.Stdin) 131 | err = m.process(scanner) 132 | 133 | if err != nil { 134 | fmt.Printf("error processing STDIN - %s\n", err.Error()) 135 | return 1 136 | } 137 | return 0 138 | } 139 | 140 | // Otherwise each named file 141 | for _, file := range args { 142 | 143 | handle, err2 := os.Open(file) 144 | if err2 != nil { 145 | fmt.Printf("error opening %s: %s\n", file, err2.Error()) 146 | return 1 147 | } 148 | 149 | reader := bufio.NewReader(handle) 150 | err = m.process(reader) 151 | if err != nil { 152 | fmt.Printf("error processing %s: %s\n", file, err.Error()) 153 | return 1 154 | } 155 | } 156 | 157 | return 0 158 | } 159 | -------------------------------------------------------------------------------- /cmd_password.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "math/rand" 7 | ) 8 | 9 | // Structure for our options and state. 10 | type passwordCommand struct { 11 | 12 | // The length of the password to generate 13 | length int 14 | 15 | // Specials? 16 | specials bool 17 | 18 | // Digits? 19 | digits bool 20 | 21 | // Confusing characters? 22 | collide bool 23 | } 24 | 25 | // Arguments adds per-command args to the object. 26 | func (p *passwordCommand) Arguments(f *flag.FlagSet) { 27 | f.IntVar(&p.length, "length", 15, "The length of the password to generate") 28 | f.BoolVar(&p.specials, "specials", true, "Should we use special characters?") 29 | f.BoolVar(&p.digits, "digits", true, "Should we use digits?") 30 | f.BoolVar(&p.collide, "ambiguous", false, "Should we allow ambiguous characters (0O1lI8B5S2ZD)?") 31 | } 32 | 33 | // Info returns the name of this subcommand. 34 | func (p *passwordCommand) Info() (string, string) { 35 | return "make-password", `Generate a random password. 36 | 37 | Details: 38 | 39 | This command generates a simple random password, by default being 12 40 | characters long. You can tweak the alphabet used via the command-line 41 | flags if necessary.` 42 | } 43 | 44 | // Execute is invoked if the user specifies `make-password` as the subcommand. 45 | func (p *passwordCommand) Execute(args []string) int { 46 | 47 | // Alphabets we use for generation 48 | // 49 | // Notice that some items are removed as "ambiguous": 50 | // 51 | // 0O1lI8B5S2ZD 52 | // 53 | digits := "34679" 54 | specials := "~=&+%^*/()[]{}/!@#$?|" 55 | upper := "ACEFGHJKLMNPQRTUVWXY" 56 | lower := "abcdefghijkmnopqrstuvwxyz" 57 | 58 | // Reinstate the missing characters, if we need to. 59 | if p.collide { 60 | digits = "0123456789" 61 | upper = "ABCDEFGHIJKLMNOPQRSTUVWXY" 62 | lower = "abcdefghijklmnopqrstuvwxy" 63 | } 64 | 65 | all := upper + lower 66 | // Extend our alphabet if we should 67 | if p.digits { 68 | all = all + digits 69 | } 70 | if p.specials { 71 | all = all + specials 72 | } 73 | 74 | // Make a buffer and fill it with all characters 75 | buf := make([]byte, p.length) 76 | for i := 0; i < p.length; i++ { 77 | buf[i] = all[rand.Intn(len(all))] 78 | } 79 | 80 | // Add a digit if we should. 81 | // 82 | // We might already have them present, because our `all` 83 | // alphabet was used already. But this ensures we have at 84 | // least one digit present. 85 | if p.digits { 86 | buf[0] = digits[rand.Intn(len(digits))] 87 | } 88 | 89 | // Add a special-character if we should. 90 | // 91 | // We might already have them present, because our `all` 92 | // alphabet was used already. But this ensures we have at 93 | // least one special-character present. 94 | if p.specials { 95 | buf[1] = specials[rand.Intn(len(specials))] 96 | } 97 | 98 | // Shuffle and output 99 | rand.Shuffle(len(buf), func(i, j int) { 100 | buf[i], buf[j] = buf[j], buf[i] 101 | }) 102 | fmt.Printf("%s\n", buf) 103 | 104 | return 0 105 | } 106 | -------------------------------------------------------------------------------- /cmd_rss.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "os" 7 | 8 | "github.com/mmcdole/gofeed" 9 | ) 10 | 11 | // Structure for our options and state. 12 | type rssCommand struct { 13 | // format contains the format-string to display for entries. 14 | // We recognize "$link" and "$title". More might be added in the future. 15 | format string 16 | } 17 | 18 | // Arguments adds per-command args to the object. 19 | func (r *rssCommand) Arguments(f *flag.FlagSet) { 20 | f.StringVar(&r.format, "format", "$link", "Specify the format-string to display for entries") 21 | } 22 | 23 | // Info returns the name of this subcommand. 24 | func (r *rssCommand) Info() (string, string) { 25 | return "rss", `Show details from an RSS feed. 26 | 27 | Details: 28 | 29 | This command fetches the specified URLs as RSS feeds, and shows 30 | their contents in a simple fashion. By default only the entry URLs 31 | are shown, but a format-string may be used to specify the output. 32 | 33 | For example to show the link and title of entries: 34 | 35 | $ sysbox rss -format='$link $title' http://,.. 36 | 37 | Suggestions for additional fields/details to be displayed are 38 | welcome via issue-reports. 39 | 40 | Format String: 41 | 42 | Currently the following values are supported: 43 | 44 | * $content The content of the entry. 45 | * $date The published date of the entry. 46 | * $guid The GUID of the entry. 47 | * $length The length of the entry. 48 | * $link The link to the entry. 49 | * $title The title of the entry. 50 | 51 | Usage: 52 | 53 | $ sysbox rss url1 url2 .. urlN 54 | 55 | Note: 56 | 57 | Care must be taken to escape, or quote, the '$' character which 58 | is used in the format-string. 59 | ` 60 | } 61 | 62 | // Process each specified feed. 63 | func (r *rssCommand) processFeed(url string) error { 64 | 65 | // Create the parser with defaults 66 | fp := gofeed.NewParser() 67 | 68 | // Parse the feed 69 | feed, err := fp.ParseURL(url) 70 | if err != nil { 71 | return err 72 | } 73 | 74 | // For each entry 75 | for _, ent := range feed.Items { 76 | 77 | // Get a piece of text, using our format-string 78 | txt := os.Expand( 79 | r.format, 80 | func(s string) string { 81 | switch s { 82 | case "content": 83 | return ent.Content 84 | case "date": 85 | return ent.Published 86 | case "guid": 87 | return ent.GUID 88 | case "length": 89 | return fmt.Sprintf("%d", len(ent.Content)) 90 | case "link": 91 | return ent.Link 92 | case "title": 93 | return ent.Title 94 | default: 95 | return s 96 | } 97 | }, 98 | ) 99 | 100 | // Now show it 101 | fmt.Println(txt) 102 | } 103 | 104 | // All good. 105 | return nil 106 | } 107 | 108 | // Execute is invoked if the user specifies `rss` as the subcommand. 109 | func (r *rssCommand) Execute(args []string) int { 110 | 111 | for _, u := range args { 112 | 113 | err := r.processFeed(u) 114 | if err != nil { 115 | fmt.Printf("Failed to process %s: %s\n", u, err) 116 | } 117 | } 118 | 119 | return 0 120 | } 121 | -------------------------------------------------------------------------------- /cmd_run_directory.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "flag" 6 | "fmt" 7 | "os" 8 | "os/exec" 9 | "path/filepath" 10 | "syscall" 11 | ) 12 | 13 | // Structure for our options and state. 14 | type runDirectoryCommand struct { 15 | 16 | // Exit on error? 17 | exit bool 18 | 19 | // Be verbose? 20 | verbose bool 21 | } 22 | 23 | // Arguments adds per-command args to the object. 24 | func (rd *runDirectoryCommand) Arguments(f *flag.FlagSet) { 25 | f.BoolVar(&rd.exit, "exit", false, "Exit if any command terminates with a non-zero exit-code") 26 | f.BoolVar(&rd.verbose, "verbose", false, "Be verbose.") 27 | } 28 | 29 | // Info returns the name of this subcommand. 30 | func (rd *runDirectoryCommand) Info() (string, string) { 31 | return "run-directory", `Run all the executables in a directory. 32 | 33 | Details: 34 | 35 | This command allows you to run each of the (executable) files in a given 36 | directory. 37 | 38 | Optionally you can terminate processing if any of the executables exit 39 | with a non-zero exit-code.` 40 | } 41 | 42 | // IsExecutable returns true if the given path points to an executable file. 43 | func (rd *runDirectoryCommand) IsExecutable(path string) bool { 44 | d, err := os.Stat(path) 45 | if err == nil { 46 | m := d.Mode() 47 | return !m.IsDir() && m&0111 != 0 48 | } 49 | return false 50 | } 51 | 52 | // RunCommand is a helper to run a command, returning output and the exit-code. 53 | func (rd *runDirectoryCommand) RunCommand(command string) (stdout string, stderr string, exitCode int) { 54 | var outbuf, errbuf bytes.Buffer 55 | cmd := exec.Command(command) 56 | cmd.Stdout = &outbuf 57 | cmd.Stderr = &errbuf 58 | 59 | err := cmd.Run() 60 | stdout = outbuf.String() 61 | stderr = errbuf.String() 62 | 63 | if err != nil { 64 | // try to get the exit code 65 | if exitError, ok := err.(*exec.ExitError); ok { 66 | ws := exitError.Sys().(syscall.WaitStatus) 67 | exitCode = ws.ExitStatus() 68 | } else { 69 | // This will happen (in OSX) if `name` is not 70 | // available in $PATH, in this situation, exit 71 | // code could not be get, and stderr will be 72 | // empty string very likely, so we use the default 73 | // fail code, and format err to string and set to stderr 74 | exitCode = 1 75 | if stderr == "" { 76 | stderr = err.Error() 77 | } 78 | } 79 | } else { 80 | // success, exitCode should be 0 if go is ok 81 | ws := cmd.ProcessState.Sys().(syscall.WaitStatus) 82 | exitCode = ws.ExitStatus() 83 | } 84 | return stdout, stderr, exitCode 85 | } 86 | 87 | // RunParts runs all the executables in the given directory. 88 | func (rd *runDirectoryCommand) RunParts(directory string) { 89 | 90 | // 91 | // Find the files beneath the named directory. 92 | // 93 | files, err := os.ReadDir(directory) 94 | if err != nil { 95 | fmt.Printf("error reading directory contents %s - %s\n", directory, err) 96 | os.Exit(1) 97 | } 98 | 99 | // 100 | // For each file we found. 101 | // 102 | for _, f := range files { 103 | 104 | // 105 | // Get the absolute path to the file. 106 | // 107 | path := filepath.Join(directory, f.Name()) 108 | 109 | // 110 | // We'll skip any dotfiles. 111 | // 112 | if f.Name()[0] == '.' { 113 | if rd.verbose { 114 | fmt.Printf("Skipping dotfile: %s\n", path) 115 | } 116 | continue 117 | } 118 | 119 | // 120 | // We'll skip any non-executable files. 121 | // 122 | if !rd.IsExecutable(path) { 123 | if rd.verbose { 124 | fmt.Printf("Skipping non-executable %s\n", path) 125 | } 126 | continue 127 | } 128 | 129 | // 130 | // Show what we're doing. 131 | // 132 | if rd.verbose { 133 | fmt.Printf("%s - launching\n", path) 134 | } 135 | 136 | // 137 | // Run the command, capturing output and exit-code 138 | // 139 | stdout, stderr, exitCode := rd.RunCommand(path) 140 | 141 | // 142 | // Show STDOUT 143 | // 144 | if len(stdout) > 0 { 145 | fmt.Print(stdout) 146 | } 147 | 148 | // 149 | // Show STDERR 150 | // 151 | if len(stderr) > 0 { 152 | 153 | fmt.Print(stderr) 154 | } 155 | 156 | // 157 | // Show the duration, if we should 158 | // 159 | if rd.verbose { 160 | fmt.Printf("%s - completed\n", path) 161 | } 162 | 163 | // 164 | // If the exit-code was non-zero then we have to 165 | // terminate. 166 | // 167 | if exitCode != 0 { 168 | if rd.verbose { 169 | fmt.Printf("%s returned non-zero exit-code\n", path) 170 | } 171 | if rd.exit { 172 | os.Exit(1) 173 | } 174 | } 175 | 176 | } 177 | } 178 | 179 | // Execute is invoked if the user specifies `run-directory` as the subcommand. 180 | func (rd *runDirectoryCommand) Execute(args []string) int { 181 | // 182 | // Ensure we have at least one argument. 183 | // 184 | if len(args) < 1 { 185 | fmt.Printf("Usage: run-directory [directory2] .. [directoryN]\n") 186 | os.Exit(1) 187 | } 188 | 189 | // 190 | // Process each named directory 191 | // 192 | for _, entry := range args { 193 | rd.RunParts(entry) 194 | } 195 | 196 | return 0 197 | } 198 | -------------------------------------------------------------------------------- /cmd_splay.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "math/rand" 7 | "strconv" 8 | "time" 9 | ) 10 | 11 | // Structure for our options and state. 12 | type splayCommand struct { 13 | max int 14 | verbose bool 15 | } 16 | 17 | // Arguments adds per-command args to the object. 18 | func (s *splayCommand) Arguments(f *flag.FlagSet) { 19 | f.IntVar(&s.max, "maximum", 300, "The maximum amount of time to sleep for") 20 | f.BoolVar(&s.verbose, "verbose", false, "Should we be verbose") 21 | 22 | } 23 | 24 | // Info returns the name of this subcommand. 25 | func (s *splayCommand) Info() (string, string) { 26 | return "splay", `Sleep for a random time. 27 | 28 | Details: 29 | 30 | This command allows you to stagger execution of things via the introduction 31 | of random delays. 32 | 33 | The expected use-case is that you have a number of hosts which each wish 34 | to perform a cron-job, but you don't want to overwhelm a central system 35 | by having all those events occur at precisely the same time (which is 36 | likely to happen if you're running with good clocks). 37 | 38 | Give each script a random-delay via adding a call to the splay subcommand. 39 | 40 | Usage: 41 | 42 | We prefer users to specify the splay-time with a parameter, but to allow 43 | natural usage you may specify as the first argument: 44 | 45 | $ sysbox splay --maximum=10 [-verbose] 46 | $ sysbox splay 10 [-verbose]` 47 | } 48 | 49 | // Execute is invoked if the user specifies `splay` as the subcommand. 50 | func (s *splayCommand) Execute(args []string) int { 51 | 52 | // If the user gave an argument then use it. 53 | // 54 | // Because people might expect this to work. 55 | if len(args) > 0 { 56 | 57 | // First argument will be a number 58 | num, err := strconv.Atoi(args[0]) 59 | if err != nil { 60 | fmt.Printf("error converting %s to integer: %s\n", args[0], err.Error()) 61 | } 62 | 63 | // Save it away. 64 | s.max = num 65 | } 66 | 67 | // Get the delay-time. 68 | delay := rand.Intn(s.max) 69 | if s.verbose { 70 | fmt.Printf("Sleeping for for %d seconds, from max splay-time of %d\n", delay, s.max) 71 | } 72 | 73 | // Sleep 74 | time.Sleep(time.Duration(delay) * time.Second) 75 | return 0 76 | } 77 | -------------------------------------------------------------------------------- /cmd_ssl_expiry.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "crypto/tls" 5 | "flag" 6 | "fmt" 7 | "net" 8 | "regexp" 9 | "strings" 10 | "time" 11 | ) 12 | 13 | // SSLExpiryCommand is the structure for our options and state. 14 | type SSLExpiryCommand struct { 15 | 16 | // Show time in hours (only) 17 | hours bool 18 | 19 | // Show time in days (only) 20 | days bool 21 | } 22 | 23 | // Arguments adds per-command args to the object. 24 | func (s *SSLExpiryCommand) Arguments(f *flag.FlagSet) { 25 | f.BoolVar(&s.hours, "hours", false, "Report only the number of hours until the certificate expires") 26 | f.BoolVar(&s.days, "days", false, "Report only the number of days until the certificate expires") 27 | 28 | } 29 | 30 | // Info returns the name of this subcommand. 31 | func (s *SSLExpiryCommand) Info() (string, string) { 32 | return "ssl-expiry", 33 | `Report how long until an SSL certificate expires. 34 | 35 | Details: 36 | 37 | This sub-command shows the number of hours/days until the SSL 38 | certificate presented upon a remote host expires. The value 39 | displayed is the minimum expiration time of the certificate and 40 | any bundled-chains served with it. 41 | 42 | Examples: 43 | 44 | Report on an SSL certificate: 45 | 46 | $ gobox ssl-expiry https://example.com/ 47 | $ gobox ssl-expiry example.com 48 | 49 | Report on an SMTP-certificate: 50 | 51 | $ gobox ssl-expiry smtp.gmail.com:465 52 | ` 53 | 54 | } 55 | 56 | // Execute runs our sub-command. 57 | func (s *SSLExpiryCommand) Execute(args []string) int { 58 | 59 | ret := 0 60 | 61 | // 62 | // Ensure we have an argument 63 | // 64 | if len(args) < 1 { 65 | fmt.Printf("You must specify the host(s) to test.\n") 66 | return 1 67 | } 68 | 69 | // For each argument 70 | for _, arg := range args { 71 | 72 | hours, err := s.SSLExpiration(arg) 73 | if err != nil { 74 | fmt.Printf("\tERROR:%s\n", err.Error()) 75 | ret = 1 76 | } else { 77 | 78 | // Output for scripting 79 | if s.hours { 80 | fmt.Printf("%s\t%d\thours\n", arg, hours) 81 | } 82 | if s.days { 83 | fmt.Printf("%s\t%d\tdays\n", arg, hours/24) 84 | } 85 | 86 | // Output for humans 87 | if !s.hours && !s.days { 88 | fmt.Printf("%s\t%d hours\t%d days\n", arg, hours, hours/24) 89 | } 90 | } 91 | } 92 | 93 | return ret 94 | } 95 | 96 | // SSLExpiration returns the number of hours remaining for a given 97 | // SSL certificate chain. 98 | func (s *SSLExpiryCommand) SSLExpiration(host string) (int64, error) { 99 | 100 | // Expiry time, in hours 101 | var hours int64 102 | hours = -1 103 | 104 | // 105 | // If the string matches http[s]://, then strip it off 106 | // 107 | re, err := regexp.Compile(`^https?:\/\/([^\/]+)`) 108 | if err != nil { 109 | return 0, err 110 | } 111 | res := re.FindAllStringSubmatch(host, -1) 112 | for _, v := range res { 113 | host = v[1] 114 | } 115 | 116 | // 117 | // If no port is specified default to :443 118 | // 119 | p := strings.Index(host, ":") 120 | if p == -1 { 121 | host += ":443" 122 | } 123 | 124 | // 125 | // Connect, with sane timeout 126 | // 127 | conn, err := tls.DialWithDialer(&net.Dialer{Timeout: time.Second * 2}, "tcp", host, nil) 128 | if err != nil { 129 | return 0, err 130 | } 131 | defer conn.Close() 132 | 133 | timeNow := time.Now() 134 | for _, chain := range conn.ConnectionState().VerifiedChains { 135 | for _, cert := range chain { 136 | 137 | // Get the expiration time, in hours. 138 | expiresIn := int64(cert.NotAfter.Sub(timeNow).Hours()) 139 | 140 | // If we've not checked anything this is the benchmark 141 | if hours == -1 { 142 | hours = expiresIn 143 | } else { 144 | // Otherwise replace our result if the 145 | // certificate is going to expire more 146 | // recently than the current "winner". 147 | if expiresIn < hours { 148 | hours = expiresIn 149 | } 150 | } 151 | } 152 | } 153 | 154 | return hours, nil 155 | } 156 | -------------------------------------------------------------------------------- /cmd_timeout.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "flag" 6 | "fmt" 7 | "io" 8 | "os" 9 | "os/exec" 10 | "time" 11 | 12 | "github.com/creack/pty" 13 | "golang.org/x/term" 14 | ) 15 | 16 | // Structure for our options and state. 17 | type timeoutCommand struct { 18 | duration int 19 | } 20 | 21 | // Arguments adds per-command args to the object. 22 | func (t *timeoutCommand) Arguments(f *flag.FlagSet) { 23 | f.IntVar(&t.duration, "timeout", 300, "The number of seconds to let the command run for") 24 | 25 | } 26 | 27 | // Info returns the name of this subcommand. 28 | func (t *timeoutCommand) Info() (string, string) { 29 | return "timeout", `Run a command, with a timeout. 30 | 31 | Details: 32 | 33 | This command allows you to execute an arbitrary command, but terminate it 34 | after the given number of seconds. 35 | 36 | The command is launched with a PTY to allow interactive commands to work 37 | as expected, for example 38 | 39 | $ sysbox timeout -duration=10 top` 40 | } 41 | 42 | // Execute is invoked if the user specifies `timeout` as the subcommand. 43 | func (t *timeoutCommand) Execute(args []string) int { 44 | 45 | if len(args) <= 0 { 46 | fmt.Printf("Usage: timeout command [arg1] [arg2] ..[argN]\n") 47 | return 1 48 | } 49 | 50 | // Create a timeout context 51 | ctx, cancel := context.WithTimeout(context.Background(), time.Duration(t.duration)*time.Second) 52 | defer cancel() 53 | 54 | // Create the command, using our context. 55 | c := exec.CommandContext(ctx, args[0], args[1:]...) 56 | 57 | // Start the command with a pty. 58 | ptmx, err := pty.Start(c) 59 | if err != nil { 60 | fmt.Printf("Failed to launch %s\n", err.Error()) 61 | return 1 62 | } 63 | 64 | // Make sure to close the pty at the end. 65 | defer func() { _ = ptmx.Close() }() 66 | 67 | // Set stdin in raw mode. 68 | oldState, err := term.MakeRaw(int(os.Stdin.Fd())) 69 | if err != nil { 70 | panic(err) 71 | } 72 | defer func() { _ = term.Restore(int(os.Stdin.Fd()), oldState) }() // Best effort. 73 | 74 | // Copy stdin to the pty and the pty to stdout/stderr. 75 | // 76 | // If any of the copy commands complete kill our context, which 77 | // will let us stop awaiting completion. 78 | go func() { 79 | io.Copy(ptmx, os.Stdin) 80 | cancel() 81 | }() 82 | go func() { 83 | io.Copy(os.Stdout, ptmx) 84 | cancel() 85 | }() 86 | go func() { 87 | io.Copy(os.Stderr, ptmx) 88 | cancel() 89 | }() 90 | 91 | // 92 | // Wait for our command to complete. 93 | // 94 | <-ctx.Done() 95 | 96 | return 0 97 | } 98 | -------------------------------------------------------------------------------- /cmd_todo.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "flag" 6 | "fmt" 7 | "os" 8 | "path/filepath" 9 | "regexp" 10 | "strings" 11 | "time" 12 | ) 13 | 14 | // Structure for our options and state. 15 | type todoCommand struct { 16 | 17 | // The current date/time 18 | now time.Time 19 | 20 | // regular expression to find (nn/NN...) 21 | reg *regexp.Regexp 22 | 23 | // silent? 24 | silent bool 25 | 26 | // verbose? 27 | verbose bool 28 | } 29 | 30 | // Arguments adds per-command args to the object. 31 | func (t *todoCommand) Arguments(f *flag.FlagSet) { 32 | f.BoolVar(&t.silent, "silent", false, "Should we be silent in the case of permission-errors?") 33 | f.BoolVar(&t.verbose, "verbose", false, "Should we report on what we're doing?") 34 | } 35 | 36 | // Info returns the name of this subcommand. 37 | func (t *todoCommand) Info() (string, string) { 38 | return "todo", `Flag TODO-notes past their expiry date. 39 | 40 | Details: 41 | 42 | This command recursively examines files beneath the current directory, 43 | or the named directory, and outputs any comments which have an associated 44 | date which is in the past. 45 | 46 | Two comment-types are supported 'TODO' and 'FIXME' - these must occur 47 | literally, and in upper-case only. To find comments which should be 48 | reported the line must also contain a date, enclosed in parenthesis. 49 | 50 | The following examples show the kind of comments that will be reported 51 | when the given date(s) are in the past: 52 | 53 | // TODO (10/03/2022) - Raise this after 10th March 2022. 54 | // TODO (03/2022) - Raise this after March 2022. 55 | // TODO (02/06/2022) - Raise this after 2nd June 2022. 56 | // FIXME - This will break at the end of the year (2023). 57 | // FIXME - RootCA must be renewed & replaced before (10/2025). 58 | 59 | Usage: 60 | 61 | $ sysbox todo 62 | $ sysbox todo ~/Projects/ 63 | 64 | ` 65 | } 66 | 67 | // Process all the files beneath the given path 68 | func (t *todoCommand) scanPath(path string) error { 69 | 70 | err := filepath.Walk(path, 71 | func(path string, info os.FileInfo, err error) error { 72 | if err != nil { 73 | if !os.IsPermission(err) { 74 | return err 75 | } 76 | 77 | if !t.silent { 78 | fmt.Fprintf(os.Stderr, "permission denied: %s\n", path) 79 | } 80 | return nil 81 | } 82 | 83 | // We only want to read files 84 | isDir := info.IsDir() 85 | 86 | if !isDir { 87 | err := t.processFile(path) 88 | return err 89 | } 90 | 91 | return nil 92 | }) 93 | 94 | return err 95 | } 96 | 97 | // processLine outputs any matching lines; those that contain a date and a TODO/FIXME reference. 98 | func (t *todoCommand) processLine(path string, line string) error { 99 | 100 | // Does this line contain TODO, or FIXME? If not return 101 | if !strings.Contains(line, "TODO") && !strings.Contains(line, "FIXME") { 102 | return nil 103 | } 104 | 105 | // remove leading/trailing space 106 | line = strings.TrimSpace(line) 107 | 108 | // Does it contain a date? 109 | match := t.reg.FindStringSubmatch(line) 110 | 111 | // OK we have a date. 112 | if len(match) >= 2 { 113 | 114 | // The date we've found 115 | date := match[1] 116 | 117 | var found time.Time 118 | var err error 119 | 120 | // Split by "/" to find the number 121 | // of values we've got: 122 | // 123 | // "DD/MM/YYYY" 124 | // "MM/YYYY" 125 | // "YYYY" 126 | parts := strings.Split(date, "/") 127 | 128 | switch len(parts) { 129 | case 3: 130 | found, err = time.Parse("02/01/2006", date) 131 | if err != nil { 132 | return fmt.Errorf("failed to parse %s:%s", date, err) 133 | } 134 | case 2: 135 | found, err = time.Parse("01/2006", date) 136 | if err != nil { 137 | return fmt.Errorf("failed to parse %s:%s", date, err) 138 | } 139 | case 1: 140 | found, err = time.Parse("2006", date) 141 | if err != nil { 142 | return fmt.Errorf("failed to parse %s:%s", date, err) 143 | } 144 | default: 145 | return fmt.Errorf("unknown date-format %s", date) 146 | } 147 | 148 | // If the date we've parsed is before today 149 | // then we alert on the line. 150 | if found.Before(t.now) { 151 | fmt.Printf("%s:%s\n", path, line) 152 | } 153 | } 154 | 155 | return nil 156 | } 157 | 158 | // processFile opens a file and reads line by line for a date. 159 | func (t *todoCommand) processFile(path string) error { 160 | 161 | if t.verbose { 162 | fmt.Printf("examining %s\n", path) 163 | } 164 | 165 | // open the file 166 | file, err := os.Open(path) 167 | if err != nil { 168 | 169 | // error - is it permission-denied? If so we can swallow that 170 | if os.IsPermission(err) { 171 | if !t.silent { 172 | fmt.Fprintf(os.Stderr, "permission denied opening: %s\n", path) 173 | } 174 | return nil 175 | } 176 | 177 | // ok another error 178 | return fmt.Errorf("failed to scan file %s:%s", path, err) 179 | } 180 | defer file.Close() 181 | 182 | // prepare to read the file 183 | scanner := bufio.NewScanner(file) 184 | 185 | // 64k is the default max length of the line-buffer - double it. 186 | const maxCapacity int = 128 * 1024 * 1024 187 | buf := make([]byte, maxCapacity) 188 | scanner.Buffer(buf, maxCapacity) 189 | 190 | // Process each line 191 | for scanner.Scan() { 192 | 193 | err := t.processLine(path, scanner.Text()) 194 | if err != nil { 195 | return err 196 | } 197 | } 198 | 199 | if err := scanner.Err(); err != nil { 200 | return err 201 | } 202 | 203 | return nil 204 | } 205 | 206 | // Execute is invoked if the user specifies `todo` as the subcommand. 207 | func (t *todoCommand) Execute(args []string) int { 208 | 209 | // Save today's date/time which we'll use for comparison. 210 | t.now = time.Now() 211 | 212 | // Create the capture regexp 213 | var err error 214 | t.reg, err = regexp.Compile(`\(([0-9/]+)\)`) 215 | if err != nil { 216 | fmt.Printf("internal error compiling regular expression:%s\n", err) 217 | return 1 218 | } 219 | 220 | // If we got any directories .. 221 | if len(args) > 0 { 222 | 223 | failed := false 224 | 225 | // process each path 226 | for _, path := range args { 227 | 228 | // error? then report it, but continue 229 | err = t.scanPath(path) 230 | if err != nil { 231 | fmt.Printf("error handling %s: %s\n", path, err) 232 | failed = true 233 | } 234 | } 235 | 236 | // exit-code will reveal errors 237 | if failed { 238 | return 1 239 | } 240 | return 0 241 | } 242 | 243 | // No named directory/directories - just handle the PWD 244 | err = t.scanPath(".") 245 | if err != nil { 246 | fmt.Printf("error handling search:%s\n", err) 247 | return 1 248 | } 249 | 250 | return 0 251 | } 252 | -------------------------------------------------------------------------------- /cmd_tree.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "os" 7 | "path/filepath" 8 | "strings" 9 | ) 10 | 11 | // Structure for our options and state. 12 | type treeCommand struct { 13 | 14 | // show only directories? 15 | directories bool 16 | 17 | // show all files? 18 | all bool 19 | } 20 | 21 | // Arguments adds per-command args to the object. 22 | func (t *treeCommand) Arguments(f *flag.FlagSet) { 23 | f.BoolVar(&t.directories, "d", false, "Show only directories.") 24 | f.BoolVar(&t.all, "a", false, "Show all files, including dotfiles.") 25 | 26 | } 27 | 28 | // Info returns the name of this subcommand. 29 | func (t *treeCommand) Info() (string, string) { 30 | return "tree", `Show filesystem contents as a tree. 31 | 32 | Details: 33 | 34 | This is a minimal reimplementation of the standard 'tree' command, it 35 | supports showing a directory tree. 36 | 37 | Usage: 38 | 39 | $ sysbox tree /etc/ 40 | 41 | To show only directory entries: 42 | 43 | $ sysbox tree -d /opt 44 | 45 | If there were any errors encountered then the return-code will be 1, otherwise 0.` 46 | } 47 | 48 | // Execute is invoked if the user specifies `tree` as the subcommand. 49 | func (t *treeCommand) Execute(args []string) int { 50 | 51 | // 52 | // Starting directory defaults to the current working directory 53 | // 54 | start := "." 55 | 56 | // 57 | // But can be changed 58 | // 59 | if len(args) > 0 { 60 | start = args[0] 61 | } 62 | 63 | type Entry struct { 64 | name string 65 | error string 66 | directory bool 67 | } 68 | 69 | // 70 | // Keep track of directory entries here. 71 | // 72 | entries := []*Entry{} 73 | 74 | // 75 | // Find the contents 76 | // 77 | filepath.Walk(start, 78 | func(path string, info os.FileInfo, err error) error { 79 | 80 | // Null info? That probably means that the 81 | // destination we're trying to walk doesn't exist. 82 | if info == nil { 83 | return nil 84 | } 85 | 86 | entry := &Entry{name: path} 87 | 88 | if err == nil { 89 | switch mode := info.Mode(); { 90 | case mode.IsDir(): 91 | entry.directory = true 92 | } 93 | } else { 94 | entry.error = err.Error() 95 | } 96 | entries = append(entries, entry) 97 | return nil 98 | }) 99 | 100 | // 101 | // Did we hit an error? 102 | // 103 | error := false 104 | 105 | // 106 | // Show the entries 107 | // 108 | for _, ent := range entries { 109 | 110 | // showing only directories? Then skip this 111 | // entry unless it is a directory 112 | if t.directories && !ent.directory { 113 | continue 114 | } 115 | 116 | // skip dotfiles by default 117 | if (strings.Contains(ent.name, "/.") || strings.HasPrefix(ent.name, ".")) && !t.all { 118 | continue 119 | } 120 | 121 | if ent.error != "" { 122 | fmt.Printf("%s - %s\n", ent.name, ent.error) 123 | error = true 124 | continue 125 | } 126 | fmt.Printf("%s\n", ent.name) 127 | } 128 | 129 | if error { 130 | return 1 131 | } 132 | return 0 133 | } 134 | -------------------------------------------------------------------------------- /cmd_urls.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "fmt" 6 | "os" 7 | "regexp" 8 | 9 | "github.com/skx/subcommands" 10 | ) 11 | 12 | // Structure for our options and state. 13 | type urlsCommand struct { 14 | 15 | // We embed the NoFlags option, because we accept no command-line flags. 16 | subcommands.NoFlags 17 | 18 | // Regular expression we find matches with. 19 | reg *regexp.Regexp 20 | } 21 | 22 | // Info returns the name of this subcommand. 23 | func (u *urlsCommand) Info() (string, string) { 24 | return "urls", `Extract URLs from text. 25 | 26 | Details: 27 | 28 | This command extracts URLs from STDIN, or the named files, and 29 | prints them. Only http and https URLs will be extracted, and we 30 | operate with a regular expression so we're a little naive. 31 | 32 | Examples: 33 | 34 | $ echo "https://example.com/ test " | sysbox urls 35 | $ sysbox urls ~/Org/bookmarks.org 36 | 37 | Limitations: 38 | 39 | Since we're doing a naive job there are limitations, the most obvious 40 | one is that we use a simple regular expression to find URLs. I've 41 | chosen break URLs when I hit a ')' or ']' character, which means markdown 42 | files can be parsed neatly. This does mean it is possible valid links 43 | will be truncated. 44 | 45 | For example Wikipedia will contain links like this, which will be truncated 46 | incorrectly: 47 | 48 | http://en.wikipedia.org/...(foo) 49 | 50 | (i.e The trailing ')' will be removed.)` 51 | } 52 | 53 | // Match our regular expression against the given reader 54 | func (u *urlsCommand) process(reader *bufio.Reader) { 55 | 56 | // 57 | // Read line by line. 58 | // 59 | // Usually we'd use bufio.Scanner, however that can 60 | // report problems with lines that are too long: 61 | // 62 | // Error: bufio.Scanner: token too long 63 | // 64 | // Instead we use the bufio.ReadString method to avoid it. 65 | // 66 | line, err := reader.ReadString(byte('\n')) 67 | for err == nil { 68 | matches := u.reg.FindAllStringSubmatch(line, -1) 69 | for _, v := range matches { 70 | if len(v) > 0 { 71 | fmt.Printf("%s\n", v[1]) 72 | } 73 | } 74 | line, err = reader.ReadString(byte('\n')) 75 | } 76 | } 77 | 78 | // Execute is invoked if the user specifies `urls` as the subcommand. 79 | func (u *urlsCommand) Execute(args []string) int { 80 | 81 | // 82 | // Naive pattern for URL matching. 83 | // 84 | // NOTE: This stops when we hit characters that are valid 85 | // for example ")", "]", ",", "'", "\", etc. 86 | // 87 | // This is helpful for Markdown documents, however it IS 88 | // wrong. 89 | // 90 | pattern := "(https?://[^\\\\\"'` \n\r\t\\]\\,)]+)" 91 | u.reg = regexp.MustCompile(pattern) 92 | 93 | // 94 | // Read from STDIN 95 | // 96 | if len(args) == 0 { 97 | 98 | scanner := bufio.NewReader(os.Stdin) 99 | 100 | u.process(scanner) 101 | 102 | return 0 103 | } 104 | 105 | // 106 | // Otherwise each named file 107 | // 108 | for _, file := range args { 109 | 110 | handle, err := os.Open(file) 111 | if err != nil { 112 | fmt.Printf("error opening %s : %s\n", file, err.Error()) 113 | return 1 114 | } 115 | 116 | reader := bufio.NewReader(handle) 117 | u.process(reader) 118 | } 119 | 120 | return 0 121 | } 122 | -------------------------------------------------------------------------------- /cmd_validate_json.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "flag" 6 | "fmt" 7 | "os" 8 | "strings" 9 | ) 10 | 11 | // Structure for our options and state. 12 | type validateJSONCommand struct { 13 | 14 | // comma-separated list of files to exclude, as set by the 15 | // command-line flag. 16 | exclude string 17 | 18 | // an array of patterns to exclude, calculated from the 19 | // exclude setting above. 20 | excluded []string 21 | 22 | // Should we report on what we're testing. 23 | verbose bool 24 | } 25 | 26 | // Arguments adds per-command args to the object. 27 | func (vj *validateJSONCommand) Arguments(f *flag.FlagSet) { 28 | f.BoolVar(&vj.verbose, "verbose", false, "Should we be verbose") 29 | f.StringVar(&vj.exclude, "exclude", "", "Comma-separated list of patterns to exclude files from the check") 30 | 31 | } 32 | 33 | // Info returns the name of this subcommand. 34 | func (vj *validateJSONCommand) Info() (string, string) { 35 | return "validate-json", `Validate all JSON files for syntax. 36 | 37 | Details: 38 | 39 | This command allows you to validate JSON files, by default searching 40 | recursively beneath the current directory for all files which match 41 | the pattern '*.json'. 42 | 43 | If you prefer you may specify a number of directories or files: 44 | 45 | - Any file specified will be checked. 46 | - Any directory specified will be recursively scanned for matching files. 47 | - Files that do not have a '.json' suffix will be ignored. 48 | 49 | Example: 50 | 51 | $ sysbox validate-json -verbose file1.json file2.json .. 52 | $ sysbox validate-json -exclude=foo /dir/1/path /file/1/path .. 53 | ` 54 | } 55 | 56 | // Validate a single file 57 | func (vj *validateJSONCommand) validateFile(path string) error { 58 | 59 | // Exclude this file? Based on the supplied list though? 60 | for _, ex := range vj.excluded { 61 | if strings.Contains(path, ex) { 62 | if vj.verbose { 63 | fmt.Printf("SKIPPED\t%s - matched '%s'\n", path, ex) 64 | } 65 | return nil 66 | } 67 | } 68 | 69 | // Read the file-contents 70 | data, err := os.ReadFile(path) 71 | if err != nil { 72 | return err 73 | } 74 | 75 | // Deserialize - receiving an error if that failed. 76 | var result interface{} 77 | err = json.Unmarshal(data, &result) 78 | 79 | // Show the error if there was one, but otherwise only show 80 | // the success if running verbosely. 81 | if err != nil { 82 | fmt.Printf("ERROR\t%s - %s\n", path, err.Error()) 83 | } else { 84 | if vj.verbose { 85 | fmt.Printf("OK\t%s\n", path) 86 | } 87 | } 88 | return err 89 | } 90 | 91 | // Execute is invoked if the user specifies `validate-json` as the subcommand. 92 | func (vj *validateJSONCommand) Execute(args []string) int { 93 | 94 | // Did we find at least one file with an error? 95 | failed := false 96 | 97 | // Create our array of excluded patterns if something 98 | // should be excluded. 99 | if vj.exclude != "" { 100 | vj.excluded = strings.Split(vj.exclude, ",") 101 | } 102 | 103 | // Add a fake argument if nothing is present, because we 104 | // want to process the current directory (recursively) by default. 105 | if len(args) < 1 { 106 | args = append(args, ".") 107 | } 108 | 109 | // We can handle file/directory names as arguments. If a 110 | // directory is specified then we process it recursively. 111 | // 112 | // We'll start by building up a list of all the files to test, 113 | // before we begin the process of testing. We'll make sure 114 | // our list is unique to cut down on any unnecessary I/O. 115 | todo := make(map[string]bool) 116 | 117 | // For each argument .. 118 | for _, arg := range args { 119 | 120 | // Check that it actually exists. 121 | info, err := os.Stat(arg) 122 | if os.IsNotExist(err) { 123 | fmt.Printf("The path does not exist: %s\n", arg) 124 | continue 125 | } 126 | 127 | // Error? 128 | if err != nil { 129 | fmt.Printf("Failed to stat(%s): %s\n", arg, err.Error()) 130 | continue 131 | } 132 | 133 | // A directory? 134 | if info.Mode().IsDir() { 135 | 136 | // Find suitable entries in the directory 137 | files, err := FindFiles(arg, []string{".json"}) 138 | if err != nil { 139 | fmt.Printf("Error finding files in %s: %s\n", arg, err.Error()) 140 | continue 141 | } 142 | 143 | // Then record each one. 144 | for _, ent := range files { 145 | todo[ent] = true 146 | } 147 | } else { 148 | 149 | // OK the entry we were given is just a file, 150 | // so we'll save the path away. 151 | todo[arg] = true 152 | } 153 | } 154 | 155 | // 156 | // Now we have a list of files to process. 157 | // 158 | for file := range todo { 159 | 160 | // Run the validation, and note the result 161 | err := vj.validateFile(file) 162 | if err != nil { 163 | failed = true 164 | } 165 | } 166 | 167 | // Setup a suitable exit-code 168 | if failed { 169 | return 1 170 | } 171 | 172 | return 0 173 | } 174 | -------------------------------------------------------------------------------- /cmd_validate_xml.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "encoding/xml" 6 | "flag" 7 | "fmt" 8 | "io" 9 | "os" 10 | "strings" 11 | ) 12 | 13 | // Structure for our options and state. 14 | type validateXMLCommand struct { 15 | 16 | // comma-separated list of files to exclude, as set by the 17 | // command-line flag. 18 | exclude string 19 | 20 | // an array of patterns to exclude, calculated from the 21 | // exclude setting above. 22 | excluded []string 23 | 24 | // Should we report on what we're testing. 25 | verbose bool 26 | } 27 | 28 | // identReader is a hack which allows us to ignore character-conversion 29 | // issues, depending on the encoded-characterset of the XML input. 30 | // 31 | // We use this because we care little for the attributes/values, instead 32 | // wanting to check for tag-validity. 33 | func identReader(encoding string, input io.Reader) (io.Reader, error) { 34 | return input, nil 35 | } 36 | 37 | // Arguments adds per-command args to the object. 38 | func (vx *validateXMLCommand) Arguments(f *flag.FlagSet) { 39 | f.BoolVar(&vx.verbose, "verbose", false, "Should we be verbose") 40 | f.StringVar(&vx.exclude, "exclude", "", "Comma-separated list of patterns to exclude files from the check") 41 | 42 | } 43 | 44 | // Info returns the name of this subcommand. 45 | func (vx *validateXMLCommand) Info() (string, string) { 46 | return "validate-xml", `Validate all XML files for syntax. 47 | 48 | Details: 49 | 50 | This command allows you to validate XML files, by default searching 51 | recursively beneath the current directory for all files which match 52 | the pattern '*.xml'. 53 | 54 | If you prefer you may specify a number of directories or files: 55 | 56 | - Any file specified will be checked. 57 | - Any directory specified will be recursively scanned for matching files. 58 | - Files that do not have a '.xml' suffix will be ignored. 59 | 60 | Example: 61 | 62 | $ sysbox validate-xml -verbose file1.xml file2.xml .. 63 | $ sysbox validate-xml -exclude=foo /dir/1/path /file/1/path .. 64 | ` 65 | } 66 | 67 | // Validate a single file 68 | func (vx *validateXMLCommand) validateFile(path string) error { 69 | 70 | // Exclude this file? Based on the supplied list though? 71 | for _, ex := range vx.excluded { 72 | if strings.Contains(path, ex) { 73 | if vx.verbose { 74 | fmt.Printf("SKIPPED\t%s - matched '%s'\n", path, ex) 75 | } 76 | return nil 77 | } 78 | } 79 | 80 | // Read the file-contents 81 | data, err := os.ReadFile(path) 82 | if err != nil { 83 | return err 84 | } 85 | 86 | // Store the results here. 87 | var result interface{} 88 | 89 | // Decode into the results, taking care that we 90 | // wire up some magic to avoid caring about the 91 | // encoding/character-set issues. 92 | r := bytes.NewReader(data) 93 | decoder := xml.NewDecoder(r) 94 | decoder.CharsetReader = identReader 95 | err = decoder.Decode(&result) 96 | 97 | // Show the error if there was one, but otherwise only show 98 | // the success if running verbosely. 99 | if err != nil { 100 | fmt.Printf("ERROR\t%s - %s\n", path, err.Error()) 101 | } else { 102 | if vx.verbose { 103 | fmt.Printf("OK\t%s\n", path) 104 | } 105 | } 106 | return err 107 | } 108 | 109 | // Execute is invoked if the user specifies `validate-xml` as the subcommand. 110 | func (vx *validateXMLCommand) Execute(args []string) int { 111 | 112 | // Did we find at least one file with an error? 113 | failed := false 114 | 115 | // Create our array of excluded patterns if something 116 | // should be excluded. 117 | if vx.exclude != "" { 118 | vx.excluded = strings.Split(vx.exclude, ",") 119 | } 120 | 121 | // Add a fake argument if nothing is present, because we 122 | // want to process the current directory (recursively) by default. 123 | if len(args) < 1 { 124 | args = append(args, ".") 125 | } 126 | 127 | // We can handle file/directory names as arguments. If a 128 | // directory is specified then we process it recursively. 129 | // 130 | // We'll start by building up a list of all the files to test, 131 | // before we begin the process of testing. We'll make sure 132 | // our list is unique to cut down on any unnecessary I/O. 133 | todo := make(map[string]bool) 134 | 135 | // For each argument .. 136 | for _, arg := range args { 137 | 138 | // Check that it actually exists. 139 | info, err := os.Stat(arg) 140 | if os.IsNotExist(err) { 141 | fmt.Printf("The path does not exist: %s\n", arg) 142 | continue 143 | } 144 | 145 | // Error? 146 | if err != nil { 147 | fmt.Printf("Failed to stat(%s): %s\n", arg, err.Error()) 148 | continue 149 | } 150 | 151 | // A directory? 152 | if info.Mode().IsDir() { 153 | 154 | // Find suitable entries in the directory 155 | files, err := FindFiles(arg, []string{".xml"}) 156 | if err != nil { 157 | fmt.Printf("Error finding files in %s: %s\n", arg, err.Error()) 158 | continue 159 | } 160 | 161 | // Then record each one. 162 | for _, ent := range files { 163 | todo[ent] = true 164 | } 165 | } else { 166 | 167 | // OK the entry we were given is just a file, 168 | // so we'll save the path away. 169 | todo[arg] = true 170 | } 171 | } 172 | 173 | // 174 | // Now we have a list of files to process. 175 | // 176 | for file := range todo { 177 | 178 | // Run the validation, and note the result 179 | err := vx.validateFile(file) 180 | if err != nil { 181 | failed = true 182 | } 183 | } 184 | 185 | // Setup a suitable exit-code 186 | if failed { 187 | return 1 188 | } 189 | 190 | return 0 191 | } 192 | -------------------------------------------------------------------------------- /cmd_validate_yaml.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "os" 7 | "strings" 8 | 9 | "gopkg.in/yaml.v2" 10 | ) 11 | 12 | // Structure for our options and state. 13 | type validateYAMLCommand struct { 14 | 15 | // comma-separated list of files to exclude 16 | exclude string 17 | 18 | // an array of patterns to exclude, calculated from the 19 | // exclude setting above 20 | excluded []string 21 | 22 | // Should we be verbose in what we're testing. 23 | verbose bool 24 | } 25 | 26 | // Arguments adds per-command args to the object. 27 | func (vy *validateYAMLCommand) Arguments(f *flag.FlagSet) { 28 | f.BoolVar(&vy.verbose, "verbose", false, "Should we be verbose") 29 | f.StringVar(&vy.exclude, "exclude", "", "Comma-separated list of patterns to exclude files from the check") 30 | } 31 | 32 | // Info returns the name of this subcommand. 33 | func (vy *validateYAMLCommand) Info() (string, string) { 34 | return "validate-yaml", `Perform syntax-checks on YAML files. 35 | 36 | Details: 37 | 38 | This command allows you to validate YAML files, by default searching 39 | recursively beneath the current directory for all files which match 40 | the patterns '*.yml', and '*.yaml'. 41 | 42 | If you prefer you may specify a number of directories or files: 43 | 44 | - Any file specified will be checked. 45 | - Any directory specified will be recursively scanned for matching files. 46 | - Files that do not have a '.yml' or '.yaml' suffix are ignored. 47 | 48 | Example: 49 | 50 | $ sysbox validate-yaml -verbose file1.yaml file2.yaml .. 51 | $ sysbox validate-yaml -exclude=foo /dir/1/path /file/1/path .. 52 | ` 53 | } 54 | 55 | // Validate a single file 56 | func (vy *validateYAMLCommand) validateFile(path string) error { 57 | 58 | // Exclude this file? Based on the supplied list though? 59 | for _, ex := range vy.excluded { 60 | if strings.Contains(path, ex) { 61 | if vy.verbose { 62 | fmt.Printf("SKIPPED\t%s - matched '%s'\n", path, ex) 63 | } 64 | return nil 65 | } 66 | } 67 | 68 | // Read the file-contents 69 | data, err := os.ReadFile(path) 70 | if err != nil { 71 | return err 72 | } 73 | 74 | // Deserialize - receiving an error if that failed. 75 | var result interface{} 76 | err = yaml.Unmarshal(data, &result) 77 | 78 | // Show the error if there was one, but otherwise only show 79 | // the success if running verbosely. 80 | if err != nil { 81 | fmt.Printf("ERROR\t%s - %s\n", path, err.Error()) 82 | } else { 83 | if vy.verbose { 84 | fmt.Printf("OK\t%s\n", path) 85 | } 86 | } 87 | return err 88 | } 89 | 90 | // Execute is invoked if the user specifies `validate-yaml` as the subcommand. 91 | func (vy *validateYAMLCommand) Execute(args []string) int { 92 | 93 | // Did we find at least one file with an error? 94 | failed := false 95 | 96 | // Create our array of excluded patterns if something 97 | // should be excluded. 98 | if vy.exclude != "" { 99 | vy.excluded = strings.Split(vy.exclude, ",") 100 | } 101 | 102 | // Add a fake argument if nothing is present, because we 103 | // want to process the current directory (recursively) by default. 104 | if len(args) < 1 { 105 | args = append(args, ".") 106 | } 107 | 108 | // We can handle file/directory names as arguments. If a 109 | // directory is specified then we process it recursively. 110 | // 111 | // We'll start by building up a list of all the files to test, 112 | // before we begin the process of testing. We'll make sure 113 | // our list is unique to cut down on any unnecessary I/O. 114 | todo := make(map[string]bool) 115 | 116 | // For each argument .. 117 | for _, arg := range args { 118 | 119 | // Check that it actually exists. 120 | info, err := os.Stat(arg) 121 | if os.IsNotExist(err) { 122 | fmt.Printf("The path does not exist: %s\n", arg) 123 | continue 124 | } 125 | 126 | // Error? 127 | if err != nil { 128 | fmt.Printf("Failed to stat(%s): %s\n", arg, err.Error()) 129 | continue 130 | } 131 | 132 | // A directory? 133 | if info.Mode().IsDir() { 134 | 135 | // Find suitable entries in the directory 136 | files, err := FindFiles(arg, []string{".yaml", ".yml"}) 137 | if err != nil { 138 | fmt.Printf("Error finding files in %s: %s\n", arg, err.Error()) 139 | continue 140 | } 141 | 142 | // Then record each one. 143 | for _, ent := range files { 144 | todo[ent] = true 145 | } 146 | } else { 147 | 148 | // OK the entry we were given is just a file, 149 | // so we'll save the path away. 150 | todo[arg] = true 151 | } 152 | } 153 | 154 | // 155 | // Now we have a list of files to process. 156 | // 157 | for file := range todo { 158 | 159 | // Run the validation, and note the result 160 | err := vy.validateFile(file) 161 | if err != nil { 162 | failed = true 163 | } 164 | } 165 | 166 | // Setup a suitable exit-code 167 | if failed { 168 | return 1 169 | } 170 | 171 | return 0 172 | } 173 | -------------------------------------------------------------------------------- /cmd_version.go: -------------------------------------------------------------------------------- 1 | //go:build !go1.18 2 | // +build !go1.18 3 | 4 | package main 5 | 6 | import ( 7 | "fmt" 8 | 9 | "github.com/skx/subcommands" 10 | ) 11 | 12 | var ( 13 | version = "unreleased" 14 | ) 15 | 16 | // Structure for our options and state. 17 | type versionCommand struct { 18 | 19 | // We embed the NoFlags option, because we accept no command-line flags. 20 | subcommands.NoFlags 21 | } 22 | 23 | // Info returns the name of this subcommand. 24 | func (t *versionCommand) Info() (string, string) { 25 | return "version", `Show the version of this binary. 26 | 27 | Details: 28 | 29 | This reports upon the version of the application. 30 | ` 31 | } 32 | 33 | // Execute is invoked if the user specifies `version` as the subcommand. 34 | func (t *versionCommand) Execute(args []string) int { 35 | 36 | fmt.Printf("%s\n", version) 37 | 38 | return 0 39 | } 40 | -------------------------------------------------------------------------------- /cmd_version_18.go: -------------------------------------------------------------------------------- 1 | //go:build go1.18 2 | // +build go1.18 3 | 4 | package main 5 | 6 | import ( 7 | "fmt" 8 | "runtime/debug" 9 | "strings" 10 | 11 | "github.com/skx/subcommands" 12 | ) 13 | 14 | var ( 15 | version = "unreleased" 16 | ) 17 | 18 | // Structure for our options and state. 19 | type versionCommand struct { 20 | 21 | // We embed the NoFlags option, because we accept no command-line flags. 22 | subcommands.NoFlags 23 | } 24 | 25 | // Info returns the name of this subcommand. 26 | func (t *versionCommand) Info() (string, string) { 27 | return "version", `Show the version of this binary. 28 | 29 | Details: 30 | 31 | This reports upon the version of the application. 32 | ` 33 | } 34 | 35 | // Execute is invoked if the user specifies `version` as the subcommand. 36 | func (t *versionCommand) Execute(args []string) int { 37 | 38 | fmt.Printf("%s\n", version) 39 | 40 | info, ok := debug.ReadBuildInfo() 41 | 42 | if ok { 43 | for _, settings := range info.Settings { 44 | if strings.Contains(settings.Key, "vcs") { 45 | fmt.Printf("%s: %s\n", settings.Key, settings.Value) 46 | } 47 | } 48 | } 49 | 50 | return 0 51 | } 52 | -------------------------------------------------------------------------------- /cmd_watch.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "os" 7 | "os/exec" 8 | "runtime" 9 | "strings" 10 | "time" 11 | 12 | "github.com/gdamore/tcell/v2" 13 | "github.com/rivo/tview" 14 | ) 15 | 16 | // Structure for our options and state. 17 | type watchCommand struct { 18 | 19 | // delay contains the number of seconds to sleep before updating our command. 20 | delay int 21 | 22 | // count increments once every second. 23 | count int 24 | 25 | // This can be set in the keyboard handler, and will trigger an immediate re-run 26 | // of the command, without disturbing the regularly scheduled update(s). 27 | immediately bool 28 | } 29 | 30 | // Arguments adds per-command args to the object. 31 | func (w *watchCommand) Arguments(f *flag.FlagSet) { 32 | f.IntVar(&w.delay, "n", 5, "The number of seconds to sleep before re-running the specified command.") 33 | } 34 | 35 | // Info returns the name of this subcommand. 36 | func (w *watchCommand) Info() (string, string) { 37 | return "watch", `Watch the output of a command. 38 | 39 | Details: 40 | 41 | This command allows you execute a command every five seconds, 42 | and see the most recent output. 43 | 44 | It is included because Mac OS does not include a watch-command 45 | by default. 46 | 47 | The display uses the tview text-based user interface package, to 48 | present a somewhat graphical display - complete with an updating 49 | run-timer. 50 | 51 | To exit the application you may press 'q', 'Escape', or Ctrl-c. 52 | 53 | ` 54 | } 55 | 56 | // Execute is invoked if the user specifies `watch` as the subcommand. 57 | func (w *watchCommand) Execute(args []string) int { 58 | 59 | if len(args) < 1 { 60 | fmt.Printf("Usage: watch cmd arg1 arg2 .. argN\n") 61 | return 1 62 | } 63 | 64 | // Command we're going to run 65 | command := strings.Join(args, " ") 66 | 67 | // Start time so that 68 | startTime := time.Now() 69 | 70 | // Assume Unix 71 | shell := "/bin/sh -c" 72 | 73 | switch runtime.GOOS { 74 | case "windows": 75 | shell = "cmd /c" 76 | } 77 | 78 | // Build up the thing to run 79 | sh := strings.Split(shell, " ") 80 | sh = append(sh, command) 81 | 82 | // Create the screen 83 | screen, err := tcell.NewScreen() 84 | if err != nil { 85 | panic(err) 86 | } 87 | err = screen.Init() 88 | if err != nil { 89 | panic(err) 90 | } 91 | 92 | // Create the application 93 | app := tview.NewApplication() 94 | app.SetScreen(screen) 95 | 96 | // Create the viewing-area 97 | viewer := tview.NewTextView() 98 | viewer.SetScrollable(true) 99 | viewer.SetBackgroundColor(tcell.ColorDefault) 100 | 101 | // 102 | // If the user presses 'q' or Esc in the viewer then exit 103 | // 104 | viewer.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey { 105 | if event.Key() == tcell.KeyEscape { 106 | app.Stop() 107 | } 108 | if event.Rune() == 'q' { 109 | app.Stop() 110 | } 111 | if event.Rune() == ' ' { 112 | // A space will trigger a re-run the next second 113 | w.immediately = true 114 | } 115 | return event 116 | }) 117 | 118 | // Create an elapsed time record 119 | elapsed := tview.NewTextView() 120 | elapsed.SetTextColor(tcell.ColorBlack) 121 | elapsed.SetTextAlign(tview.AlignRight) 122 | elapsed.SetText("0s") 123 | elapsed.SetBackgroundColor(tcell.ColorGreen) 124 | 125 | // Setup a title 126 | title := tview.NewTextView() 127 | title.SetTextColor(tcell.ColorBlack) 128 | title.SetText(fmt.Sprintf("%s every %ds", command, w.delay)) 129 | title.SetBackgroundColor(tcell.ColorGreen) 130 | 131 | // The status-bar will have the title and elapsed time 132 | statusBar := tview.NewFlex() 133 | statusBar.AddItem(title, 0, 1, false) 134 | statusBar.AddItem(elapsed, 15, 1, false) 135 | 136 | // The layout will have the status-bar 137 | flex := tview.NewFlex().SetDirection(tview.FlexRow) 138 | flex.AddItem(viewer, 0, 1, true) 139 | flex.AddItem(statusBar, 1, 1, false) 140 | app.SetRoot(flex, true) 141 | 142 | // Ensure we update 143 | go func() { 144 | run := true 145 | 146 | for { 147 | 148 | // Run the command if we should, either: 149 | // 150 | // 1. The first time we start. 151 | // 152 | // 2. When the timer has exceeded our second-count 153 | if run || w.immediately { 154 | 155 | // Command output 156 | var out []byte 157 | 158 | // Run the command and get the output 159 | cmd := exec.Command(sh[0], sh[1:]...) 160 | 161 | // Get the output. 162 | out, err = cmd.CombinedOutput() 163 | if err != nil { 164 | app.Stop() 165 | fmt.Printf("Error running command: %v - %s\n", sh, err) 166 | os.Exit(1) 167 | } 168 | 169 | // Once we've done that we're all ready to update the screen 170 | app.QueueUpdateDraw(func() { 171 | 172 | // Clear the screen 173 | screen.Clear() 174 | 175 | // Update the main-window's output 176 | viewer.SetText(tview.TranslateANSI(string(out))) 177 | 178 | // And update our run-time log 179 | elapsed.SetText(fmt.Sprintf("%v", time.Since(startTime).Round(time.Second))) 180 | }) 181 | 182 | run = false 183 | } else { 184 | 185 | // Otherwise just update the status-bars elapsed timer. 186 | app.QueueUpdateDraw(func() { 187 | elapsed.SetText(fmt.Sprintf("%v", time.Since(startTime).Round(time.Second))) 188 | }) 189 | } 190 | 191 | // We sleep for a second, and want to reset the to-run flag when we've done that 192 | // enough times. 193 | w.count++ 194 | if w.count >= w.delay { 195 | w.count = 0 196 | run = true 197 | } 198 | 199 | // The user can press the space-bar to trigger an immediate run, 200 | // reset the flag that would have been set in that case. 201 | if w.immediately { 202 | w.immediately = false 203 | } 204 | 205 | // delay before the next test. 206 | time.Sleep(time.Second) 207 | } 208 | }() 209 | 210 | // Run the application 211 | err = app.Run() 212 | if err != nil { 213 | fmt.Printf("Error in watch:%s\n", err) 214 | return 1 215 | } 216 | 217 | return 0 218 | } 219 | -------------------------------------------------------------------------------- /cmd_with_lock.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "crypto/sha1" 6 | "flag" 7 | "fmt" 8 | "os" 9 | "os/exec" 10 | "path/filepath" 11 | 12 | "github.com/nightlyone/lockfile" 13 | ) 14 | 15 | // Structure for our options and state. 16 | type withLockCommand struct { 17 | 18 | // prefix is the directory-root beneath which we write our lockfile. 19 | prefix string 20 | 21 | // lockFile contains the name of the user-supplied lockfile to use, 22 | // if this is set then one will not be constructed automatically 23 | // and prefix will be ignored. 24 | lockFile string 25 | } 26 | 27 | // Arguments adds per-command args to the object. 28 | func (wl *withLockCommand) Arguments(f *flag.FlagSet) { 29 | f.StringVar(&wl.prefix, "prefix", "/var/tmp", "The location beneath which to write our lockfile") 30 | f.StringVar(&wl.lockFile, "lock", "", "Specify a lockfile here directly, fully-qualified, if you don't want an auto-constructed one.") 31 | } 32 | 33 | // Info returns the name of this subcommand. 34 | func (wl *withLockCommand) Info() (string, string) { 35 | return "with-lock", `Execute a process, with a lock. 36 | 37 | Details: 38 | 39 | This command allows you to execute a command, with arguments, 40 | using a lockfile. This will prevent multiple concurrent executions 41 | of the same command. 42 | 43 | The expected use-case is to prevent overlapping executions of cronjobs, 44 | etc. 45 | 46 | Implementation: 47 | 48 | A filename is constructed based upon the command to be executed, and 49 | this is used to prevent the concurrent execution. The command, and 50 | arguments, to be executed are passed through a SHA1 hash for consistency. 51 | 52 | The -lock flag may be used to supply a fully-qualified lockfile path, 53 | in the case where a lockfile collision might be expected - in that case 54 | the -prefix argument is ignored. 55 | ` 56 | } 57 | 58 | // Execute is invoked if the user specifies `with-lock` as the subcommand. 59 | func (wl *withLockCommand) Execute(args []string) int { 60 | 61 | // 62 | // Ensure we have an argument 63 | // 64 | if len(args) < 1 { 65 | fmt.Printf("You must specify the command to execute\n") 66 | return 1 67 | } 68 | 69 | // 70 | // Generate a lockfile 71 | // 72 | h := sha1.New() 73 | for i, arg := range args { 74 | h.Write([]byte(fmt.Sprintf("%d:%s", i, arg))) 75 | } 76 | hash := fmt.Sprintf("%x", h.Sum(nil)) 77 | 78 | // 79 | // The actual path will go here 80 | // 81 | path := filepath.Join(wl.prefix, string(hash)) 82 | 83 | // 84 | // If the user specified a complete path then that will 85 | // be used instead. 86 | // 87 | if wl.lockFile != "" { 88 | path = wl.lockFile 89 | } 90 | 91 | // 92 | // Create the lockfile 93 | // 94 | lock, err := lockfile.New(path) 95 | if err != nil { 96 | fmt.Printf("Cannot init lockfile (%s). reason: %v", path, err) 97 | return 1 98 | } 99 | 100 | // Error handling is essential, as we only try to get the lock. 101 | if err = lock.TryLock(); err != nil { 102 | fmt.Printf("Cannot lock %q (%s), reason: %v", lock, path, err) 103 | return 1 104 | } 105 | 106 | defer func() { 107 | if errr := lock.Unlock(); errr != nil { 108 | fmt.Printf("Cannot unlock %q (%s), reason: %v", lock, path, errr) 109 | os.Exit(1) 110 | } 111 | }() 112 | 113 | // 114 | // Run the command. 115 | // 116 | cmd := exec.Command(args[0], args[1:]...) 117 | var stdout bytes.Buffer 118 | cmd.Stdout = &stdout 119 | var stderr bytes.Buffer 120 | cmd.Stderr = &stderr 121 | err = cmd.Run() 122 | 123 | if len(stdout.String()) > 0 { 124 | fmt.Print(stdout.String()) 125 | } 126 | if len(stderr.String()) > 0 { 127 | fmt.Print(stderr.String()) 128 | } 129 | if err != nil { 130 | fmt.Printf("Error running command:%s\n", err.Error()) 131 | return 1 132 | } 133 | 134 | return 0 135 | } 136 | -------------------------------------------------------------------------------- /common.go: -------------------------------------------------------------------------------- 1 | // common.go - some routines that are used by multiple sub-commands 2 | 3 | package main 4 | 5 | import ( 6 | "os" 7 | "path/filepath" 8 | "strings" 9 | ) 10 | 11 | // FindFiles finds any file beneath the given prefix-directory which contains 12 | // a suffix included in the list. 13 | func FindFiles(path string, suffixes []string) ([]string, error) { 14 | 15 | var results []string 16 | 17 | err := filepath.Walk(path, func(path string, f os.FileInfo, err error) error { 18 | 19 | // Null info? That probably means that the 20 | // destination we're trying to walk doesn't exist. 21 | if f == nil { 22 | return nil 23 | } 24 | 25 | if !f.IsDir() { 26 | for _, suffix := range suffixes { 27 | if strings.HasSuffix(path, suffix) { 28 | results = append(results, path) 29 | } 30 | } 31 | } 32 | return err 33 | }) 34 | 35 | return results, err 36 | } 37 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/skx/sysbox 2 | 3 | go 1.23 4 | 5 | toolchain go1.23.2 6 | 7 | require ( 8 | github.com/creack/pty v1.1.24 9 | github.com/gdamore/tcell/v2 v2.7.4 10 | github.com/google/goexpect v0.0.0-20210430020637-ab937bf7fd6f 11 | github.com/mmcdole/gofeed v1.3.0 12 | github.com/nightlyone/lockfile v1.0.0 13 | github.com/peterh/liner v1.2.2 14 | github.com/rivo/tview v0.0.0-20241103174730-c76f7879f592 15 | github.com/skx/subcommands v0.9.2 16 | golang.org/x/net v0.30.0 17 | golang.org/x/term v0.25.0 18 | gopkg.in/yaml.v2 v2.4.0 19 | ) 20 | 21 | require ( 22 | github.com/PuerkitoBio/goquery v1.10.0 // indirect 23 | github.com/andybalholm/cascadia v1.3.2 // indirect 24 | github.com/gdamore/encoding v1.0.1 // indirect 25 | github.com/google/goterm v0.0.0-20200907032337-555d40f16ae2 // indirect 26 | github.com/json-iterator/go v1.1.12 // indirect 27 | github.com/kr/pretty v0.3.1 // indirect 28 | github.com/lucasb-eyer/go-colorful v1.2.0 // indirect 29 | github.com/mattn/go-runewidth v0.0.16 // indirect 30 | github.com/mmcdole/goxpp v1.1.1 // indirect 31 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 32 | github.com/modern-go/reflect2 v1.0.2 // indirect 33 | github.com/rivo/uniseg v0.4.7 // indirect 34 | golang.org/x/crypto v0.28.0 // indirect 35 | golang.org/x/sys v0.26.0 // indirect 36 | golang.org/x/text v0.19.0 // indirect 37 | google.golang.org/genproto/googleapis/rpc v0.0.0-20241104194629-dd2ea8efbc28 // indirect 38 | google.golang.org/grpc v1.67.1 // indirect 39 | google.golang.org/protobuf v1.35.1 // indirect 40 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect 41 | ) 42 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 2 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 3 | github.com/PuerkitoBio/goquery v1.10.0 h1:6fiXdLuUvYs2OJSvNRqlNPoBm6YABE226xrbavY5Wv4= 4 | github.com/PuerkitoBio/goquery v1.10.0/go.mod h1:TjZZl68Q3eGHNBA8CWaxAN7rOU1EbDz3CWuolcO5Yu4= 5 | github.com/andybalholm/cascadia v1.3.2 h1:3Xi6Dw5lHF15JtdcmAHD3i1+T8plmv7BQ/nsViSLyss= 6 | github.com/andybalholm/cascadia v1.3.2/go.mod h1:7gtRlve5FxPPgIgX36uWBX58OdBsSS6lUvCFb+h7KvU= 7 | github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= 8 | github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= 9 | github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= 10 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 11 | github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s= 12 | github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE= 13 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 14 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 15 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 16 | github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= 17 | github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= 18 | github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= 19 | github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg= 20 | github.com/gdamore/encoding v1.0.1 h1:YzKZckdBL6jVt2Gc+5p82qhrGiqMdG/eNs6Wy0u3Uhw= 21 | github.com/gdamore/encoding v1.0.1/go.mod h1:0Z0cMFinngz9kS1QfMjCP8TY7em3bZYeeklsSDPivEo= 22 | github.com/gdamore/tcell/v2 v2.7.4 h1:sg6/UnTM9jGpZU+oFYAsDahfchWAFW8Xx2yFinNSAYU= 23 | github.com/gdamore/tcell/v2 v2.7.4/go.mod h1:dSXtXTSK0VsW1biw65DZLZ2NKr7j0qP/0J7ONmsraWg= 24 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= 25 | github.com/golang/glog v1.2.2 h1:1+mZ9upx1Dh6FmUTFR1naJ77miKiXgALjWOZ3NVFPmY= 26 | github.com/golang/glog v1.2.2/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w= 27 | github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 28 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 29 | github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 30 | github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= 31 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= 32 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 33 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 34 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 35 | github.com/google/goexpect v0.0.0-20210430020637-ab937bf7fd6f h1:7MmqygqdeJtziBUpm4Z9ThROFZUaVGaePMfcDnluf1E= 36 | github.com/google/goexpect v0.0.0-20210430020637-ab937bf7fd6f/go.mod h1:n1ej5+FqyEytMt/mugVDZLIiqTMO+vsrgY+kM6ohzN0= 37 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 38 | github.com/google/goterm v0.0.0-20190703233501-fc88cf888a3f/go.mod h1:nOFQdrUlIlx6M6ODdSpBj1NVA+VgLC6kmw60mkw34H4= 39 | github.com/google/goterm v0.0.0-20200907032337-555d40f16ae2 h1:CVuJwN34x4xM2aT4sIKhmeib40NeBPhRihNjQmpJsA4= 40 | github.com/google/goterm v0.0.0-20200907032337-555d40f16ae2/go.mod h1:nOFQdrUlIlx6M6ODdSpBj1NVA+VgLC6kmw60mkw34H4= 41 | github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= 42 | github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= 43 | github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= 44 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 45 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 46 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 47 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 48 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 49 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 50 | github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= 51 | github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= 52 | github.com/mattn/go-runewidth v0.0.3/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= 53 | github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= 54 | github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= 55 | github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= 56 | github.com/mmcdole/gofeed v1.3.0 h1:5yn+HeqlcvjMeAI4gu6T+crm7d0anY85+M+v6fIFNG4= 57 | github.com/mmcdole/gofeed v1.3.0/go.mod h1:9TGv2LcJhdXePDzxiuMnukhV2/zb6VtnZt1mS+SjkLE= 58 | github.com/mmcdole/goxpp v1.1.1 h1:RGIX+D6iQRIunGHrKqnA2+700XMCnNv0bAOOv5MUhx8= 59 | github.com/mmcdole/goxpp v1.1.1/go.mod h1:v+25+lT2ViuQ7mVxcncQ8ch1URund48oH+jhjiwEgS8= 60 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 61 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= 62 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 63 | github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= 64 | github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= 65 | github.com/nightlyone/lockfile v1.0.0 h1:RHep2cFKK4PonZJDdEl4GmkabuhbsRMgk/k3uAmxBiA= 66 | github.com/nightlyone/lockfile v1.0.0/go.mod h1:rywoIealpdNse2r832aiD9jRk8ErCatROs6LzC841CI= 67 | github.com/peterh/liner v1.2.2 h1:aJ4AOodmL+JxOZZEL2u9iJf8omNRpqHc/EbrK+3mAXw= 68 | github.com/peterh/liner v1.2.2/go.mod h1:xFwJyiKIXJZUKItq5dGHZSTBRAuG/CpeNpWLyiNRNwI= 69 | github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= 70 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 71 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 72 | github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 73 | github.com/rivo/tview v0.0.0-20241103174730-c76f7879f592 h1:YIJ+B1hePP6AgynC5TcqpO0H9k3SSoZa2BGyL6vDUzM= 74 | github.com/rivo/tview v0.0.0-20241103174730-c76f7879f592/go.mod h1:02iFIz7K/A9jGCvrizLPvoqr4cEIx7q54RH5Qudkrss= 75 | github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= 76 | github.com/rivo/uniseg v0.4.3/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= 77 | github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= 78 | github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= 79 | github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= 80 | github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= 81 | github.com/skx/subcommands v0.9.2 h1:wG035k1U7Fn6A0hwOMg1ly7085cl62gnzLY1j78GISo= 82 | github.com/skx/subcommands v0.9.2/go.mod h1:HpOZHVUXT5Rc/Q7UCiyj7h5u6BleDfFjt+vxy2igonA= 83 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 84 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 85 | github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= 86 | github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 87 | github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 88 | github.com/ziutek/telnet v0.0.0-20180329124119-c3b780dc415b/go.mod h1:IZpXDfkJ6tWD3PhBK5YzgQT+xJWh7OsdwiG8hA2MkO4= 89 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 90 | golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 91 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 92 | golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw= 93 | golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U= 94 | golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 95 | golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 96 | golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= 97 | golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 98 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= 99 | golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= 100 | golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 101 | golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 102 | golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 103 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 104 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 105 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 106 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 107 | golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= 108 | golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= 109 | golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= 110 | golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4= 111 | golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU= 112 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 113 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 114 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 115 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 116 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 117 | golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 118 | golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 119 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 120 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 121 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 122 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 123 | golang.org/x/sys v0.0.0-20211117180635-dee7805ff2e1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 124 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 125 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 126 | golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 127 | golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 128 | golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 129 | golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= 130 | golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 131 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 132 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 133 | golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= 134 | golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY= 135 | golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= 136 | golang.org/x/term v0.25.0 h1:WtHI/ltw4NvSUig5KARz9h521QvRC8RmF/cuYqifU24= 137 | golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M= 138 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 139 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 140 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 141 | golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 142 | golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= 143 | golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= 144 | golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= 145 | golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= 146 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 147 | golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 148 | golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= 149 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 150 | golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 151 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 152 | golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= 153 | golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= 154 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 155 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 156 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= 157 | google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 158 | google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= 159 | google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= 160 | google.golang.org/genproto/googleapis/rpc v0.0.0-20241104194629-dd2ea8efbc28 h1:XVhgTWWV3kGQlwJHR3upFWZeTsei6Oks1apkZSeonIE= 161 | google.golang.org/genproto/googleapis/rpc v0.0.0-20241104194629-dd2ea8efbc28/go.mod h1:GX3210XPVPUjJbTUbvwI8f2IpZDMZuPJWDzDuebbviI= 162 | google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= 163 | google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= 164 | google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= 165 | google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= 166 | google.golang.org/grpc v1.67.1 h1:zWnc1Vrcno+lHZCOofnIMvycFcc0QRGIzm9dhnDX68E= 167 | google.golang.org/grpc v1.67.1/go.mod h1:1gLDyUQU7CTLJI90u3nXZ9ekeghjeM7pTDZlqFNg2AA= 168 | google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA= 169 | google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= 170 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 171 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 172 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 173 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 174 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 175 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 176 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 177 | honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 178 | honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 179 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "github.com/skx/subcommands" 8 | ) 9 | 10 | // Recovery is good 11 | func recoverPanic() { 12 | if os.Getenv("DEBUG") != "" { 13 | return 14 | } 15 | 16 | if r := recover(); r != nil { 17 | fmt.Printf("recovered from panic while running %v\n%s\n", os.Args, r) 18 | fmt.Printf("To see the panic run 'export DEBUG=on' and repeat.\n") 19 | } 20 | } 21 | 22 | // Register the subcommands, and run the one the user chose. 23 | func main() { 24 | 25 | // 26 | // Catch errors 27 | // 28 | defer recoverPanic() 29 | 30 | // 31 | // Register each of our subcommands. 32 | // 33 | subcommands.Register(&SSLExpiryCommand{}) 34 | subcommands.Register(&calcCommand{}) 35 | subcommands.Register(&chooseFileCommand{}) 36 | subcommands.Register(&chooseSTDINCommand{}) 37 | subcommands.Register(&chronicCommand{}) 38 | subcommands.Register(&collapseCommand{}) 39 | subcommands.Register(&commentsCommand{}) 40 | subcommands.Register(&cppCommand{}) 41 | subcommands.Register(&envTemplateCommand{}) 42 | subcommands.Register(&execSTDINCommand{}) 43 | subcommands.Register(&expectCommand{}) 44 | subcommands.Register(&feedsCommand{}) 45 | subcommands.Register(&findCommand{}) 46 | subcommands.Register(&fingerdCommand{}) 47 | subcommands.Register(&html2TextCommand{}) 48 | subcommands.Register(&httpGetCommand{}) 49 | subcommands.Register(&httpdCommand{}) 50 | subcommands.Register(&ipsCommand{}) 51 | subcommands.Register(&markdownTOCCommand{}) 52 | subcommands.Register(&passwordCommand{}) 53 | subcommands.Register(&rssCommand{}) 54 | subcommands.Register(&runDirectoryCommand{}) 55 | subcommands.Register(&splayCommand{}) 56 | subcommands.Register(&timeoutCommand{}) 57 | subcommands.Register(&todoCommand{}) 58 | subcommands.Register(&treeCommand{}) 59 | subcommands.Register(&urlsCommand{}) 60 | subcommands.Register(&validateJSONCommand{}) 61 | subcommands.Register(&validateXMLCommand{}) 62 | subcommands.Register(&validateYAMLCommand{}) 63 | subcommands.Register(&versionCommand{}) 64 | subcommands.Register(&watchCommand{}) 65 | subcommands.Register(&withLockCommand{}) 66 | 67 | // 68 | // Execute the one the user chose. 69 | // 70 | os.Exit(subcommands.Execute()) 71 | } 72 | -------------------------------------------------------------------------------- /templatedcmd/templatedcmd.go: -------------------------------------------------------------------------------- 1 | // Package templatedcmd allows expanding command-lines via a simple 2 | // template-expansion process. 3 | // 4 | // For example the user might wish to run a command with an argument 5 | // like so: 6 | // 7 | // command {} 8 | // 9 | // But we also support expanding the input into fields, and selecting 10 | // only a single one, as per: 11 | // 12 | // $ echo "one two" | echo {1} 13 | // # -> "one" 14 | // 15 | // All arguments are available via "{}" and "{N}" will refer to the 16 | // Nth field of the given input. 17 | package templatedcmd 18 | 19 | import ( 20 | "regexp" 21 | "strconv" 22 | "strings" 23 | ) 24 | 25 | // Expand performs the expansion of the given input, via the supplied 26 | // template. As we allow input to be referred to as an array of fields 27 | // we also let the user specify a split-string here. 28 | // 29 | // By default the input is split on whitespace, but you may supply another 30 | // string instead. 31 | func Expand(template string, input string, split string) []string { 32 | 33 | // 34 | // Regular expression for looking for ${1}, "${2}", "${3}", etc. 35 | // 36 | reg := regexp.MustCompile("({[0-9]+})") 37 | 38 | // 39 | // Trim our input of leading/trailing spaces. 40 | // 41 | input = strings.TrimSpace(input) 42 | 43 | // 44 | // Default to splitting the input on white-space. 45 | // 46 | fields := strings.Fields(input) 47 | if split != "" { 48 | fields = strings.Split(input, split) 49 | } 50 | 51 | // 52 | // The return-value is an array of strings 53 | // 54 | cmd := []string{} 55 | 56 | // 57 | // We'll operate upon a temporary copy of our template, 58 | // split into fields. 59 | // 60 | cmdTmp := strings.Fields(template) 61 | 62 | // 63 | // For each piece of the template-string look for 64 | // "{}", and "{N}", expand appropriately. 65 | // 66 | for _, piece := range cmdTmp { 67 | 68 | // 69 | // Do we have a "{N}" ? 70 | // 71 | matches := reg.FindAllStringSubmatch(piece, -1) 72 | 73 | // 74 | // If so for each match, perform the expansion 75 | // 76 | for _, v := range matches { 77 | 78 | // 79 | // Copy the match and remove the {} 80 | // 81 | // So we just have "1", "3", etc. 82 | // 83 | match := v[1] 84 | match = strings.ReplaceAll(match, "{", "") 85 | match = strings.ReplaceAll(match, "}", "") 86 | 87 | // 88 | // Convert the string to a number, and if that 89 | // worked we'll replace it with the appropriately 90 | // numbered field. 91 | // 92 | num, err := strconv.Atoi(match) 93 | if err == nil { 94 | 95 | // 96 | // If the field matches then we can replace it 97 | // 98 | if num >= 1 && num <= len(fields) { 99 | piece = strings.ReplaceAll(piece, v[1], fields[num-1]) 100 | } else { 101 | // 102 | // Otherwise it's a field that doesn't 103 | // exist. So it's replaced with ''. 104 | // 105 | piece = strings.ReplaceAll(piece, v[1], "") 106 | } 107 | } 108 | } 109 | 110 | // 111 | // Now replace "{}" with the complete argument 112 | // 113 | piece = strings.ReplaceAll(piece, "{}", input) 114 | 115 | // And append 116 | cmd = append(cmd, piece) 117 | } 118 | 119 | // 120 | // Now we should have an array of expanded strings. 121 | // 122 | return cmd 123 | } 124 | -------------------------------------------------------------------------------- /templatedcmd/templatedcmd_test.go: -------------------------------------------------------------------------------- 1 | package templatedcmd 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | // Basic test 8 | func TestBasic(t *testing.T) { 9 | 10 | type TestCase struct { 11 | template string 12 | input string 13 | split string 14 | expected []string 15 | } 16 | 17 | tests := []TestCase{ 18 | {"xine {}", "This is a file", "", []string{"xine", "This is a file"}}, 19 | {"xine {1}", "This is a file", "", []string{"xine", "This"}}, 20 | {"xine {3}", "This is a file", "", []string{"xine", "a"}}, 21 | {"xine {10}", "This is a file", "", []string{"xine", ""}}, 22 | {"foo bar", "", "", []string{"foo", "bar"}}, 23 | {"id {1}", "root:0:0...", ":", []string{"id", "root"}}, 24 | } 25 | 26 | for _, test := range tests { 27 | 28 | out := Expand(test.template, test.input, test.split) 29 | 30 | if len(out) != len(test.expected) { 31 | t.Fatalf("Expected to have %d pieces, found %d", len(test.expected), len(out)) 32 | } 33 | 34 | for i, x := range test.expected { 35 | 36 | if out[i] != x { 37 | t.Errorf("expected '%s' for piece %d, got '%s'", x, i, out[i]) 38 | } 39 | } 40 | } 41 | } 42 | --------------------------------------------------------------------------------