From a54ab0ac7e0662523133c6dbcd07366ab3e7e5c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20SZKIBA?= Date: Thu, 11 Jan 2024 20:29:04 +0100 Subject: [PATCH] feat: run subcommand --- .golangci.yml | 2 + README.md | 76 ++++++++++++++++-- go.mod | 4 + go.sum | 18 +++++ internal/cmd/dump.go | 2 +- internal/cmd/extract.go | 2 +- internal/cmd/help/filtering.md | 2 +- internal/cmd/help/run.md | 13 +++ internal/cmd/list.go | 10 ++- internal/cmd/options.go | 3 + internal/cmd/root.go | 32 +++++--- internal/cmd/run.go | 140 +++++++++++++++++++++++++++++++++ internal/cmd/update.go | 2 +- releases/v0.2.0.md | 24 ++++++ 14 files changed, 308 insertions(+), 22 deletions(-) create mode 100644 internal/cmd/help/run.md create mode 100644 internal/cmd/run.go create mode 100644 releases/v0.2.0.md diff --git a/.golangci.yml b/.golangci.yml index 29fba2b..1548f0d 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -34,6 +34,8 @@ linters-settings: - github.com/spf13/cobra - github.com/gobwas/glob - github.com/liamg/memoryfs + - mvdan.cc/sh/v3/interp + - mvdan.cc/sh/v3/syntax - github.com/szkiba/mdcode/internal deny: - pkg: io/ioutil diff --git a/README.md b/README.md index f150431..8024d07 100644 --- a/README.md +++ b/README.md @@ -78,6 +78,19 @@ This document includes the necessary code for testing within invisible code bloc Code blocks embedded in this document can be saved to files using the [`mdcode extract`](#mdcode-extract) command. A `README_test.go` and a `README.test.js` file will be created in the current directory. After modification, the code blocks can be updated from these files to the document using the [`mdcode update`](#mdcode-update) command. +After the modification, it is advisable to test the above examples using the following commands: + +```sh name=test +go test ./... +node --test +``` + +Since the above code block has a name (`test`), it can also be run with the [`mdcode run`](#mdcode-run) command: + +``` +mdcode run -n test +``` + More examples can be found in the [examples](examples/) directory and in the [tutorial](docs/testable-markdown-code-blocks.md). ### Features @@ -239,11 +252,11 @@ Or if only block comments can be used (CSS): /* #endregion */ -Regions marked this way are used by IDEs to collapse parts of the source code. +Regions marked in this way are used by IDEs to collapse parts of the source code. In the case of `mdcode`, regions can be referenced with the `region` metadata. If a region is specified for a code block, the subcommand (update or extract) applies only to the specified region of the file. That is, the update command only embeds the specified region from the file to the markdown document, and the extract command overwrites only the specified region in the file. -`mdcode` can handle regions in any programming language, the only requirement is that the comments indicating the beginning and end of the region are placed in separate lines containing only the given comment. +`mdcode` can handle regions in any programming language, the only requirement is that the comment indicating the beginning and end of the region is placed in a separate line containing only the given comment. ### Invisible @@ -435,7 +448,7 @@ The optional argument of the `mdcode` command is the name of the markdown file. ``` -mdcode [filename] [flags] +mdcode [flags] [filename] ``` ### Flags @@ -453,6 +466,7 @@ mdcode [filename] [flags] * [mdcode dump](#mdcode-dump) - Dump markdown code blocks * [mdcode extract](#mdcode-extract) - Extract markdown code blocks to the file system +* [mdcode run](#mdcode-run) - Run shell commands on markdown code blocks * [mdcode update](#mdcode-update) - Update markdown code blocks from the file system --- @@ -472,7 +486,7 @@ The optional argument of the `mdcode dump` command is the name of the markdown f ``` -mdcode dump [filename] [flags] +mdcode dump [flags] [filename] ``` ### Flags @@ -513,7 +527,7 @@ The optional argument of the `mdcode extract` command is the name of the markdow ``` -mdcode extract [filename] [flags] +mdcode extract [flags] [filename] ``` ### Flags @@ -536,6 +550,54 @@ mdcode extract [filename] [flags] * [mdcode](#mdcode) - Markdown code block authoring tool +--- +## mdcode run + +Run shell commands on markdown code blocks + +### Synopsis + +Extract code blocks to the file system and run shell commands on them + +The code blocks are written to the file named in the `file` metadata. + +The code block may include `region` metadata, which contains the name of the region. In this case, the code block is written to the appropriate part of the file marked with the `#region` comment. + +The optional argument of the `mdcode run` command is the name of the markdown file. If it is missing, the `README.md` file in the current directory (if it exists) is processed. + +This can be followed by a double dash (`--`) and then the shell command line to be executed (even a complex command, such as `for`). + +Alternatively, the commands to be executed can be embedded in a code block in the document. In this case, the language must be `sh` and it is necessary to name the code block with the metadata `name`. The name of the code block containing the commands can be specified with the `--name` flag (if not, the first code block containing the `sh` language and `name` metadata will be executed). + +Code blocks are extracted to a temporary directory. This directory will be the current directory when running the commands. The temporary directory is deleted after executing the commands (deletion can be prevented by using the `--keep` flag). Instead of a temporary directory, the name of the directory to be used can be specified with the `--dir` flag. In this case, of course, the directory is not deleted after executing the commands. + + +``` +mdcode run [flags] [filename] [-- commands] +``` + +### Flags + +``` + -d, --dir string base directory name (default ".") + -h, --help help for run + -k, --keep don't remove temporary directory + -n, --name string code block name contains commands + -q, --quiet suppress the status output +``` + +### Global Flags + +``` + -f, --file strings file filter (default [?*]) + -l, --lang strings language filter (default [?*]) + -m, --meta stringToString metadata filter (default []) +``` + +### SEE ALSO + +* [mdcode](#mdcode) - Markdown code block authoring tool + --- ## mdcode update @@ -553,7 +615,7 @@ The optional argument of the `mdcode update` command is the name of the markdown ``` -mdcode update [filename] [flags] +mdcode update [flags] [filename] ``` ### Flags @@ -576,4 +638,4 @@ mdcode update [filename] [flags] * [mdcode](#mdcode) - Markdown code block authoring tool - \ No newline at end of file + diff --git a/go.mod b/go.mod index 47b21f6..df35073 100644 --- a/go.mod +++ b/go.mod @@ -10,6 +10,7 @@ require ( github.com/spf13/cobra v1.8.0 github.com/stretchr/testify v1.7.1 github.com/yuin/goldmark v1.6.0 + mvdan.cc/sh/v3 v3.7.0 ) require ( @@ -20,5 +21,8 @@ require ( github.com/pmezard/go-difflib v1.0.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/spf13/pflag v1.0.5 // indirect + golang.org/x/sync v0.2.0 // indirect + golang.org/x/sys v0.8.0 // indirect + golang.org/x/term v0.8.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 625f5a3..80d5eb4 100644 --- a/go.sum +++ b/go.sum @@ -1,8 +1,12 @@ github.com/cpuguy83/go-md2man/v2 v2.0.3 h1:qMCsGGgs+MAzDFyp9LpAe1Lqy/fY/qCovCm0qnXZOBM= github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= +github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/frankban/quicktest v1.14.5 h1:dfYrrRyLtiqT9GyKXgdh+k4inNeTvmGbuSgZ3lx3GhA= +github.com/frankban/quicktest v1.14.5/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= @@ -11,6 +15,10 @@ github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaU github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/liamg/memoryfs v1.6.0 h1:jAFec2HI1PgMTem5gR7UT8zi9u4BfG5jorCRlLH06W8= github.com/liamg/memoryfs v1.6.0/go.mod h1:z7mfqXFQS8eSeBBsFjYLlxYRMRyiPktytvYCYTb3BSk= github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= @@ -22,6 +30,8 @@ github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rodaine/table v1.1.0 h1:/fUlCSdjamMY8VifdQRIu3VWZXYLY7QHFkVorS8NTr4= github.com/rodaine/table v1.1.0/go.mod h1:Qu3q5wi1jTQD6B6HsP6szie/S4w1QUQ8pq22pz9iL8g= +github.com/rogpeppe/go-internal v1.10.1-0.20230524175051-ec119421bb97 h1:3RPlVWzZ/PDqmVuf/FKHARG5EMid/tl7cv54Sw/QRVY= +github.com/rogpeppe/go-internal v1.10.1-0.20230524175051-ec119421bb97/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0= @@ -34,8 +44,16 @@ github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMT github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/yuin/goldmark v1.6.0 h1:boZcn2GTjpsynOsC0iJHnBWa4Bi0qzfJjthwauItG68= github.com/yuin/goldmark v1.6.0/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +golang.org/x/sync v0.2.0 h1:PUR+T4wwASmuSTYdKjYHI5TD22Wy5ogLU5qZCOLxBrI= +golang.org/x/sync v0.2.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.8.0 h1:n5xxQn2i3PC0yLAbjTpNT85q/Kgzcr2gIoX9OrJUols= +golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +mvdan.cc/sh/v3 v3.7.0 h1:lSTjdP/1xsddtaKfGg7Myu7DnlHItd3/M2tomOcNNBg= +mvdan.cc/sh/v3 v3.7.0/go.mod h1:K2gwkaesF/D7av7Kxl0HbF5kGOd2ArupNTX3X44+8l8= diff --git a/internal/cmd/dump.go b/internal/cmd/dump.go index 3efbc9c..43dca3a 100644 --- a/internal/cmd/dump.go +++ b/internal/cmd/dump.go @@ -18,7 +18,7 @@ var dumpHelp string func dumpCmd(opts *options) *cobra.Command { cmd := &cobra.Command{ //nolint:exhaustruct - Use: "dump [filename]", + Use: "dump [flags] [filename]", Aliases: []string{"d"}, Short: "Dump markdown code blocks", Long: dumpHelp, diff --git a/internal/cmd/extract.go b/internal/cmd/extract.go index b3a2568..1c4ce0a 100644 --- a/internal/cmd/extract.go +++ b/internal/cmd/extract.go @@ -18,7 +18,7 @@ var extractHelp string func extractCmd(opts *options) *cobra.Command { cmd := &cobra.Command{ //nolint:exhaustruct - Use: "extract [filename]", + Use: "extract [flags] [filename]", Aliases: []string{"x"}, Short: "Extract markdown code blocks to the file system", Long: extractHelp, diff --git a/internal/cmd/help/filtering.md b/internal/cmd/help/filtering.md index 3c490c6..c21a12e 100644 --- a/internal/cmd/help/filtering.md +++ b/internal/cmd/help/filtering.md @@ -1,4 +1,4 @@ -By default, `mdcode` work with all code blocks in a markdown document. It is possible to filter code blocks based on programming language or metadata. In this case, `mdcode` ignore code blocks that do not meet the filter criteria. +By default, `mdcode` work with all code blocks in a markdown document. It is possible to filter code blocks based on programming language or metadata. In this case, `mdcode` ignores code blocks that do not meet the filter criteria. A language filter pattern can be specified using the `--lang` flag. Then only code blocks with a language matching the pattern will be processed. For example, filtering for code blocks containing JavaScript code: diff --git a/internal/cmd/help/run.md b/internal/cmd/help/run.md new file mode 100644 index 0000000..c44b755 --- /dev/null +++ b/internal/cmd/help/run.md @@ -0,0 +1,13 @@ +Extract code blocks to the file system and run shell commands on them + +The code blocks are written to the file named in the `file` metadata. + +The code block may include `region` metadata, which contains the name of the region. In this case, the code block is written to the appropriate part of the file marked with the `#region` comment. + +The optional argument of the `mdcode run` command is the name of the markdown file. If it is missing, the `README.md` file in the current directory (if it exists) is processed. + +This can be followed by a double dash (`--`) and then the shell command line to be executed (even a complex command, such as `for`). + +Alternatively, the commands to be executed can be embedded in a code block in the document. In this case, the language must be `sh` and it is necessary to name the code block with the metadata `name`. The name of the code block containing the commands can be specified with the `--name` flag (if not, the first code block containing the `sh` language and `name` metadata will be executed). + +Code blocks are extracted to a temporary directory. This directory will be the current directory when running the commands. The temporary directory is deleted after executing the commands (deletion can be prevented by using the `--keep` flag). Instead of a temporary directory, the name of the directory to be used can be specified with the `--dir` flag. In this case, of course, the directory is not deleted after executing the commands. diff --git a/internal/cmd/list.go b/internal/cmd/list.go index 10e0333..f66ca4a 100644 --- a/internal/cmd/list.go +++ b/internal/cmd/list.go @@ -18,7 +18,13 @@ func listRun(filename string, out io.Writer, opts *options) error { return err } - blocks, err := unfence(src, opts.filter) + blocks, err := unfence(src, func(lang string, meta mdcode.Meta) bool { + if isScript(lang, meta) { + return true + } + + return opts.filter(lang, meta) + }) if err != nil { return err } @@ -99,7 +105,7 @@ func metaKeys(blocks mdcode.Blocks) []string { special := make(map[string]struct{}) - for _, s := range []string{metaFile, metaOutline, metaRegion} { + for _, s := range []string{metaName, metaFile, metaOutline, metaRegion} { special[s] = struct{}{} if _, has := keyset[s]; has { diff --git a/internal/cmd/options.go b/internal/cmd/options.go index 67878c2..80e3e44 100644 --- a/internal/cmd/options.go +++ b/internal/cmd/options.go @@ -10,6 +10,7 @@ const ( metaFile = "file" metaRegion = "region" metaOutline = "outline" + metaName = "name" ) type statusFunc func(format string, args ...any) @@ -17,6 +18,7 @@ type statusFunc func(format string, args ...any) type options struct { lang []string file []string + name string meta map[string]string dir string @@ -25,6 +27,7 @@ type options struct { json bool quiet bool + keep bool filter filterFunc status statusFunc diff --git a/internal/cmd/root.go b/internal/cmd/root.go index 4665591..7d41af7 100644 --- a/internal/cmd/root.go +++ b/internal/cmd/root.go @@ -7,6 +7,7 @@ import ( "io" "os" "path/filepath" + "strings" "github.com/spf13/cobra" ) @@ -34,7 +35,7 @@ func RootCmd() *cobra.Command { opts := new(options) cmd := &cobra.Command{ //nolint:exhaustruct - Use: appname + " [filename]", + Use: appname + " [flags] [filename]", Short: "Markdown code block authoring tool", Long: rootHelp, Version: version, @@ -73,11 +74,7 @@ func RootCmd() *cobra.Command { `{{with .Name}}{{printf "%s" .}}{{end}}{{printf " version %s\n" .Version}}`, ) - flags := cmd.PersistentFlags() - - flags.StringSliceVarP(&opts.file, "file", "f", []string{"?*"}, "file filter") - flags.StringSliceVarP(&opts.lang, "lang", "l", []string{"?*"}, "language filter") - flags.StringToStringVarP(&opts.meta, "meta", "m", nil, "metadata filter") + globalFlags(cmd, opts) outputFlag(cmd, opts) @@ -86,12 +83,21 @@ func RootCmd() *cobra.Command { cmd.AddCommand(updateCmd(opts)) cmd.AddCommand(extractCmd(opts)) cmd.AddCommand(dumpCmd(opts)) + cmd.AddCommand(runCmd(opts)) cmd.AddCommand(metadataTopic(), filteringTopic(), regionsTopic(), invisibleTopic(), outlineTopic()) return cmd } +func globalFlags(cmd *cobra.Command, opts *options) { + flags := cmd.PersistentFlags() + + flags.StringSliceVarP(&opts.file, "file", "f", []string{"?*"}, "file filter") + flags.StringSliceVarP(&opts.lang, "lang", "l", []string{"?*"}, "language filter") + flags.StringToStringVarP(&opts.meta, "meta", "m", nil, "metadata filter") +} + func outputFlag(cmd *cobra.Command, opts *options) { cmd.Flags().StringVarP(&opts.out, "output", "o", "", "output file (default: standard output)") @@ -106,11 +112,11 @@ func dirFlag(cmd *cobra.Command, opts *options) { func quietFlag(cmd *cobra.Command, opts *options) { cmd.Flags().BoolVarP(&opts.quiet, "quiet", "q", false, "suppress the status output") - - cobra.CheckErr(cmd.MarkFlagDirname("dir")) } -func checkargs(_ *cobra.Command, args []string) error { +func checkargs(cmd *cobra.Command, args []string) error { + _, args = script(cmd, args) + if len(args) > 1 { return errTooManyArg } @@ -153,6 +159,14 @@ func source(args []string) string { return args[0] } +func script(cmd *cobra.Command, args []string) (string, []string) { + if cmd.ArgsLenAtDash() < 0 { + return "", args + } + + return strings.Join(args[cmd.ArgsLenAtDash():], " "), args[:cmd.ArgsLenAtDash()] +} + const ( defaultArg = "README.md" diff --git a/internal/cmd/run.go b/internal/cmd/run.go new file mode 100644 index 0000000..926f064 --- /dev/null +++ b/internal/cmd/run.go @@ -0,0 +1,140 @@ +package cmd + +import ( + "context" + _ "embed" + "errors" + "fmt" + "os" + "regexp" + "strings" + + "github.com/spf13/cobra" + "github.com/szkiba/mdcode/internal/mdcode" + "mvdan.cc/sh/v3/interp" + "mvdan.cc/sh/v3/syntax" +) + +//go:embed help/run.md +var runHelp string + +func runCmd(opts *options) *cobra.Command { + cmd := &cobra.Command{ //nolint:exhaustruct + Use: "run [flags] [filename] [-- commands]", + Aliases: []string{"r"}, + Short: "Run shell commands on markdown code blocks", + Long: runHelp, + Args: checkargs, + PreRun: func(cmd *cobra.Command, _ []string) { + opts.createStatus(cmd.ErrOrStderr()) + }, + RunE: func(cmd *cobra.Command, args []string) error { + script, args := script(cmd, args) + + if !cmd.Flag("dir").Changed { + dir, err := os.MkdirTemp(".", "mdcode-tmp-") + if err != nil { + return err + } + + opts.dir = dir + + if !opts.keep { + defer os.RemoveAll(dir) + } + } + + return runRun(source(args), opts, script) + }, + DisableAutoGenTag: true, + } + + dirFlag(cmd, opts) + quietFlag(cmd, opts) + + cmd.Flags().StringVarP(&opts.name, "name", "n", "", "code block name contains commands") + cmd.Flags().BoolVarP(&opts.keep, "keep", "k", false, "don't remove temporary directory") + + return cmd +} + +var reShell = regexp.MustCompile("(ba|z)?sh") + +func isScript(lang string, meta mdcode.Meta) bool { + return reShell.MatchString(lang) && len(meta.Get(metaName)) != 0 +} + +func findScript(filename string, opts *options) (string, error) { + src, err := os.ReadFile(filename) + if err != nil { + return "", err + } + + var script string + + _, _, err = mdcode.Walk(src, func(block *mdcode.Block) error { + if len(script) != 0 { + return nil + } + + if !isScript(block.Lang, block.Meta) { + return nil + } + + if len(opts.name) == 0 { + script = string(block.Code) + + return nil + } + + if block.Meta.Get(metaName) == opts.name { + script = string(block.Code) + } + + return nil + }) + if err != nil { + return "", err + } + + if len(script) == 0 { + if len(opts.name) != 0 { + return "", fmt.Errorf("%w: %s", errMissingScript, opts.name) + } + + return "", fmt.Errorf("%w: '-- commands' argument required", errMissingScript) + } + + return script, nil +} + +func runRun(filename string, opts *options, script string) error { + if len(script) == 0 { + value, err := findScript(filename, opts) + if err != nil { + return err + } + + script = value + } + + if err := extractRun(filename, opts); err != nil { + return err + } + + opts.status("Executing in %s\n%s\n", opts.dir, script) + + file, err := syntax.NewParser().Parse(strings.NewReader(script), "") + if err != nil { + return err + } + + runner, err := interp.New(interp.Dir(opts.dir), interp.StdIO(os.Stdin, os.Stdout, os.Stderr)) + if err != nil { + return err + } + + return runner.Run(context.TODO(), file) +} + +var errMissingScript = errors.New("missing script") diff --git a/internal/cmd/update.go b/internal/cmd/update.go index a414a4d..ac5c575 100644 --- a/internal/cmd/update.go +++ b/internal/cmd/update.go @@ -17,7 +17,7 @@ var updateHelp string func updateCmd(opts *options) *cobra.Command { cmd := &cobra.Command{ //nolint:exhaustruct - Use: "update [filename]", + Use: "update [flags] [filename]", Aliases: []string{"u"}, Short: "Update markdown code blocks from the file system", Long: updateHelp, diff --git a/releases/v0.2.0.md b/releases/v0.2.0.md new file mode 100644 index 0000000..e962a37 --- /dev/null +++ b/releases/v0.2.0.md @@ -0,0 +1,24 @@ +mdcode `v0.2.0` is here 🎉! + +The main feature of this release is the run subcommand. + +## mdcode run + +Run shell commands on markdown code blocks + +### Synopsis + +Extract code blocks to the file system and run shell commands on them + +The code blocks are written to the file named in the `file` metadata. + +The code block may include `region` metadata, which contains the name of the region. In this case, the code block is written to the appropriate part of the file marked with the `#region` comment. + +The optional argument of the `mdcode run` command is the name of the markdown file. If it is missing, the `README.md` file in the current directory (if it exists) is processed. + +This can be followed by a double dash (`--`) and then the shell command line to be executed (even a complex command, such as `for`). + +Alternatively, the commands to be executed can be embedded in a code block in the document. In this case, the language must be `sh` and it is necessary to name the code block with the metadata `name`. The name of the code block containing the commands can be specified with the `--name` flag (if not, the first code block containing the `sh` language and `name` metadata will be executed). + +Code blocks are extracted to a temporary directory. This directory will be the current directory when running the commands. The temporary directory is deleted after executing the commands (deletion can be prevented by using the `--keep` flag). Instead of a temporary directory, the name of the directory to be used can be specified with the `--dir` flag. In this case, of course, the directory is not deleted after executing the commands. +