cmdr

package module
v2.0.16 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jan 10, 2025 License: Apache-2.0 Imports: 9 Imported by: 6

README

cmdr

Go GitHub go.mod Go version GitHub tag (latest SemVer) GoDoc FOSSA Status go.dev Go Report Card codecov Mentioned in Awesome Go

Unstable v2: Working in progress, the main API might be stable till v2.1.0

cmdr is a POSIX-compliant, command-line argument parser library with Golang.

Since v2, our license moved to Apache 2.0.

cover

See the image frames at #1.

Motivation

There are many dirty codes in the cmdr.v1 which cannot be refactored as well. It prompted we reimplment a new one as v2.

The passing winter, we did rewrite the cmdr.v2 to keep it clean and absorbed in parsing and dispatching. Some abilities were removed and relayouted to new modules. That's why the Option Store has been split as a standalone module hedzr/store[^1]. A faster and colorful slog-like logger has been implemented freshly as hedzr/logg[^3]. hedzr/evendeep[^2] provides a deep fully-functional object copy tool. It helps to deep copy some internal objects easily. It is also ready for you. hedzr/is[^4] is an environment detecting framework with many out-of-the-box detectors, such as is.InTesting and is.InDebugging.

Anyway, the whole supply chain painted:

graph BT
  hzis(hedzr/is)-->hzlogg(hedzr/logg/slog)
  hzis-->hzdiff(hedzr/evendeep)
  hzlogg-->hzdiff
  hzerrors(gopkg.in/hedzr/errors.v3)-->hzdiff
  hzerrors-->hzstore(hedzr/store)
  hzis-->hzstore(hedzr/store)
  hzlogg-->hzstore(hedzr/store)
  hzdiff-->hzstore(hedzr/store)
  hzlogg-->cmdr(hedzr/cmdr/v2)
  hzis-->cmdr
  hzlogg-->cmdr
  hzdiff-->cmdr
  hzstore-->cmdr

  1. The .netCore version Cmdr.Core is available now.
  2. A cxx version cmdr-cxx was released (Happy Spring Festival 2021).

Features

v2 is in earlier state but the baseline is stable:

  • Basic command-line arguments parser like POSIX getopt and go stdlib flag.

    • Short flag, single character or a string here to support golang CLI style

      • Compact flags if possible. Also the sticking value will be parsed. For example: -c1b23zv = -c 1 -b 23 -z -v
      • Hit info: -v -v -v = -v (hitCount == 3, hitTitle == 'v')
      • Optimized for slice: -a 1,2,3 -a 4 -a 5,6 => []int{1,2,3,4,5,6}
      • Value can be sticked or not. Valid forms: -c1, -c 1, -c=1 and quoted: -c"1", -c'1', -c="1", -c='1', etc.
      • ...
    • Long flags and aliases

    • Eventual subcommands: an OnAction handler can be attached.

    • Eventual subcommands and flags: PreActions, PostAction, OnMatching, OnMatched, ...,

    • Auto bind to environment variables, For instance: command line HELP=1 app = app --help.

    • Builtin commands and flags:

      • --help, -h
      • --version, -V
      • --verbose. -v
      • ...
    • Help Screen: auto generate and print

    • Smart suggestions when wrong cmd or flag parsed. Jaro-winkler distance is used.

  • Loosely parse subcmds and flags:

    • Subcommands and flags can be input in any order
    • Lookup a flag along with subcommands tree for resolving the duplicated flags
  • Can integrate with hedzr/store[^1]

    • High-performance in-memory KV store for hierarchical data.
    • Extract data to user-spec type with auto-converting
    • Loadable external sources: environ, config files, consul, etcd, etc..
      • extensible codecs and providers for loading from data sources
  • Three kinds of config files are searched and loaded via loaders.NewConfigFileLoader():

    • Primary: main config, shipped with installable package.
    • Secondary: 2ndry config. Wrapped by reseller(s).
    • Alternative: user's local config, writeable. The runtime changeset will be written back to this file while app stopping.
  • TODO

    • Shell autocompletion
    • ...

[^1]: hedzr/store is a high-performance configure management library [^2]: hedzr/evendeep offers a customizable deepcopy tool to you. There are also deepequal, deepdiff tools in it. [^3]: hedzr/logg provides a slog like and colorful logging library [^4]: hedzr/is is a basic environ detectors library

More minor details need to be evaluated and reimplemented if it's still meaningful in v2.

cmdr-loaders

Since v2.0.3, loaders had been splitted as a standalone repo so that we can keep cmdr v2 smaller and independer. See the relevant subproject cmdr-loaders.

History

v2 is staying in earlier state:

Guide

A simple cli-app can be:

package main

// Simplest tiny app

import (
    "context"
    "io"
    "os"

    "gopkg.in/hedzr/errors.v3"

    "github.com/hedzr/cmdr/v2"
    "github.com/hedzr/cmdr/v2/cli"
    "github.com/hedzr/cmdr/v2/pkg/dir"
    logz "github.com/hedzr/logg/slog"
    "github.com/hedzr/store"
)

func main() {
    ctx := context.Background() // with cancel can be passed thru in your actions
    app := prepareApp(
        cmdr.WithStore(store.New()), // use an option store explicitly, or a dummy store by default

        // cmdr.WithExternalLoaders(
        //     local.NewConfigFileLoader(), // import "github.com/hedzr/cmdr-loaders/local" to get in advanced external loading features
        //     local.NewEnvVarLoader(),
        // ),

        cmdr.WithTasksBeforeRun(func(ctx context.Context, cmd cli.Cmd, runner cli.Runner, extras ...any) (err error) {
            logz.DebugContext(ctx, "command running...", "cmd", cmd, "runner", runner, "extras", extras)
            return
        }),

        // true for debug in developing time, it'll disable onAction on each Cmd.
        // for productive mode, comment this line.
        // The envvars FORCE_DEFAULT_ACTION & FORCE_RUN can override this.
        // cmdr.WithForceDefaultAction(true),

        // cmdr.WithAutoEnvBindings(true),
    )
    if err := app.Run(ctx); err != nil {
        logz.ErrorContext(ctx, "Application Error:", "err", err) // stacktrace if in debug mode/build
        os.Exit(app.SuggestRetCode())
    }
}

func prepareApp(opts ...cli.Opt) (app cli.App) {
    app = cmdr.New(opts...).
        Info("tiny-app", "0.3.1").
        Author("The Example Authors") // .Description(``).Header(``).Footer(``)

    // another way to disable `cmdr.WithForceDefaultAction(true)` is using
    // env-var FORCE_RUN=1 (builtin already).
    app.Flg("no-default").
        Description("disable force default action").
        // Group(cli.UnsortedGroup).
        OnMatched(func(f *cli.Flag, position int, hitState *cli.MatchState) (err error) {
            if b, ok := hitState.Value.(bool); ok {
                // disable/enable the final state about 'force default action'
                f.Set().Set("app.force-default-action", b)
            }
            return
        }).
        Build()

    app.Cmd("jump").
        Description("jump command").
        Examples(`jump example`). // {{.AppName}}, {{.AppVersion}}, {{.DadCommands}}, {{.Commands}}, ...
        Deprecated(`v1.1.0`).
        // Group(cli.UnsortedGroup).
        Hidden(false).
        // Both With(cb) and Build() to end a building sequence
        With(func(b cli.CommandBuilder) {
            b.Cmd("to").
                Description("to command").
                Examples(``).
                Deprecated(`v0.1.1`).
                OnAction(func(ctx context.Context, cmd cli.Cmd, args []string) (err error) {
                    // cmd.Set() == cmdr.Store(), cmd.Store() == cmdr.Store()
                    cmd.Set().Set("app.demo.working", dir.GetCurrentDir())
                    println()
                    println(cmd.Set().WithPrefix("app.demo").MustString("working"))

                    cs := cmdr.Store().WithPrefix("jump.to")
                    if cs.MustBool("full") {
                        println()
                        println(cmd.Set().Dump())
                    }
                    cs2 := cmd.Store()
                    if cs2.MustBool("full") != cs.MustBool("full") {
                        logz.Panic("a bug found")
                    }
                    app.SetSuggestRetCode(1) // ret code must be in 0-255
                    return                   // handling command action here
                }).
                With(func(b cli.CommandBuilder) {
                    b.Flg("full", "f").
                        Default(false).
                        Description("full command").
                        Build()
                })
        })

    app.Flg("dry-run", "n").
        Default(false).
        Description("run all but without committing").
        Build()

    app.Flg("wet-run", "w").
        Default(false).
        Description("run all but with committing").
        Build() // no matter even if you're adding the duplicated one.

    app.Cmd("wrong").
        Description("a wrong command to return error for testing").
        // cmdline `FORCE_RUN=1 go run ./tiny wrong -d 8s` to verify this command to see the returned application error.
        OnAction(func(ctx context.Context, cmd cli.Cmd, args []string) (err error) {
            dur := cmd.Store().MustDuration("duration")
            println("the duration is:", dur.String())

            ec := errors.New()
            defer ec.Defer(&err) // store the collected errors in native err and return it
            ec.Attach(io.ErrClosedPipe, errors.New("something's wrong"), os.ErrPermission)
            // see the application error by running `go run ./tiny/tiny/main.go wrong`.
            return
        }).
        With(func(b cli.CommandBuilder) {
            b.Flg("duration", "d").
                Default("5s").
                Description("a duration var").
                Build()
        })
    return
}

More examples please go to cmdr-tests/examples.

Thanks to JODL

Thanks to JetBrains for donating product licenses to help develop cmdr
jetbrains goland

License

Since v2, our license moved to Apache 2.0.

The v1 keeps under MIT itself.

FOSSA Status

Documentation

Index

Constants

View Source
const Version = "v2.0.16" // Version fir hedzr/cmdr/v2

Variables

This section is empty.

Functions

func App

func App() cli.Runner

App returns a light version of builder.Runner (a.k.a. *worker.Worker).

Generally it's a unique instance in one system.

It's available once New() / Exec() called, else nil.

App returns a cli.Runner instance, which is different with builder.App.

func AppDescription added in v2.0.7

func AppDescription() string

func AppDescriptionLong added in v2.0.7

func AppDescriptionLong() string

func AppName added in v2.0.7

func AppName() string

func AppVersion added in v2.0.7

func AppVersion() string

func CacheDir added in v2.0.7

func CacheDir(base ...string) string

CacheDir returns standard cachedir associated with this app. In general, it would be /var/cache/<appName>, $HOME/.cache/<appName>, and so on.

CacheDir returns the default root directory to use for user-specific cached data. Users should create their own application-specific subdirectory within this one and use that.

On Unix systems, it returns $XDG_CACHE_HOME as specified by https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html if non-empty, else $HOME/.cache. On Darwin, it would be $HOME/.cache/<appName>. ~~it returns $HOME/Library/Caches.~~ On Windows, it returns %LocalAppData%/<appName>. On Plan 9, it returns $home/lib/cache/<appName>.

If the location cannot be determined (for example, $HOME is not defined), then it will return an error.

func CmdLines added in v2.0.7

func CmdLines() []string

CmdLines returns the whole command-line as space-separated slice.

func ConfigDir added in v2.0.7

func ConfigDir(base ...string) string

ConfigDir returns standard configdir associated with this app. In general, it would be /etc/<appName>, $HOME/.config/<appName>, and so on.

ConfigDir returns the default root directory to use for user-specific configuration data. Users should create their own application-specific subdirectory within this one and use that.

On Unix systems, it returns $XDG_CONFIG_HOME as specified by https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html if non-empty, else $HOME/.config/<appName>. On Darwin, it would be $HOME/.config/<appName>. ~~it returns $HOME/Library/Application Support.~~ On Windows, it returns %AppData%/<appName>. On Plan 9, it returns $home/lib/<appName>.

If the location cannot be determined (for example, $HOME is not defined), then it will return an error.

func DataDir added in v2.0.7

func DataDir(base ...string) string

DataDir returns standard datadir associated with this app. In general, it would be /usr/local/share/<appName>, $HOME/.local/share/<appName>, and so on.

DataDir is used to store app normal runtime data.

For darwin and linux it's generally at "$HOME/.local/share/<app>", or "/usr/local/share/<app>" and "/usr/share/<app>" in some builds.

For windows it is "%APPDATA%/<app>/Data".

In your application, it shall look up config files from ConfigDir, save the runtime data (or persistent data) into DataDir, use CacheDir to store the cache data which can be file and folder or file content indexes, the response replies from remote api, and so on. TempDir is used to store any temporary content which can be erased at any time.

UsrLibDir is the place which an application should be installed at, in linux.

VarRunDir is the place which a .pid, running socket file handle, and others files that can be shared in all processes of this application, sometimes for any apps.

func Exec

func Exec(rootCmd *cli.RootCommand, opts ...cli.Opt) (err error)

Exec starts a new cmdr app (parsing cmdline args based on the given rootCmd) from scratch.

It's a reserved API for back-compatible with cmdr v1.

It'll be removed completely at the recently future version.

Deprecated since 2.1 by app.Run()

func HomeDir added in v2.0.7

func HomeDir() string

HomeDir returns the current user's home directory. In general, it would be /Users/<username>, /home/<username>, etc.

func LoadedSources added in v2.0.9

func LoadedSources() []cli.LoadedSources

func New

func New(opts ...cli.Opt) cli.App

New starts a new cmdr app.

With the returned builder.App, you may build root and sub-commands fluently.

app := cmdr.New().
    Info("demo-app", "0.3.1").
    Author("hedzr")
app.Cmd("jump").
	Description("jump command").
	Examples(`jump example`).
	Deprecated(`jump is a demo command`).
	With(func(b cli.CommandBuilder) {
		b.Hidden(false)
		b.Cmd("to").
			Description("to command").
			Examples(``).
			Deprecated(`v0.1.1`).
			Hidden(false).
			OnAction(func(cmd *cli.CmdS, args []string) (err error) {
				main1()
				return // handling command action here
			}).
			With(func(b cli.CommandBuilder) {
				b.Flg("full", "f").
					Default(false).
					Description("full command").
					Build()
			})
	})
app.Flg("dry-run", "n").
    Default(false).
    Build() // no matter even if you're adding the duplicated one.

// // simple run the parser of app and trigger the matched command's action
// _ = app.Run(
//     cmdr.WithForceDefaultAction(false), // true for debug in developing time
// )

if err := app.Run(
    cmdr.WithForceDefaultAction(false), // true for debug in developing time
); err != nil {
    logz.ErrorContext(ctx, "Application Error:", "err", err)
}

After the root command and all its children are built, use app.[config.App.Run] to parse end-user's command-line arguments, and invoke the bound action on the hit subcommand.

It is not necessary to attach an action onto a parent command, because its subcommands are the main characters - but you still can do that.

func Parsed added in v2.0.7

func Parsed() bool

func ParsedCommands added in v2.0.7

func ParsedCommands() []cli.Cmd

func ParsedLastCmd added in v2.0.7

func ParsedLastCmd() cli.Cmd

func ParsedPositionalArgs added in v2.0.7

func ParsedPositionalArgs() []string

func RemoveOrderedPrefix added in v2.0.7

func RemoveOrderedPrefix(s string) string

RemoveOrderedPrefix removes '[a-z0-9]+\.' at front of string.

func Set added in v2.0.9

func Set() store.Store

Set returns the KVStore associated with current App().

func Store added in v2.0.7

func Store() store.Store

Store returns the child Store at location 'app.cmd'.

By default, cmdr maintains all command-line subcommands and flags as a child tree in the associated Set ("store") internally.

You can check out the flags state by querying in this child store.

For example, we have a command 'server'->'start' and its flag 'foreground', therefore we can query the flag what if it was given by user's 'app server start --foreground':

fore := cmdr.Store().MustBool("server.start.foreground", false)
if fore {
   runRealMain()
} else {
   service.Start("start", runRealMain) // start the real main as a service
}

// second form:
cs := cmdr.Store().WithPrefix("server.start")
fore := cs.MustBool("foreground")
port := cs.MustInt("port", 7893)

Q: How to inspect the internal Store()?

A: Running `app [any subcommands] [any options] ~~debug` will dump the internal Store() tree.

Q: Can I list all subcommands?

A: Running `app ~~tree`, `app -v ~~tree` or `app ~~tree -vvv` can get a list of subcommands tree, and with those builtin hidden commands, and with those vendor hidden commands. In this case, `-vvv` dumps the hidden commands and vendor-hidden commands.

func TempDir added in v2.0.7

func TempDir(base ...string) string

func TempFileName added in v2.0.7

func TempFileName(fileNamePattern, defaultFileName string, base ...string) (filename string)

func UsrLibDir added in v2.0.7

func UsrLibDir(base ...string) string

UsrLibDir is the runtime temp dir. "/usr/local/lib/<app>/" or "/usr/lib/<app>" in root mode.

func VarLogDir added in v2.0.9

func VarLogDir(base ...string) string

VarLogDir is todo, not exact right yet.

func VarRunDir added in v2.0.7

func VarRunDir(base ...string) string

VarRunDir is the runtime temp dir. "/var/run/<app>/"

func WithAutoEnvBindings added in v2.0.9

func WithAutoEnvBindings(b bool, prefix ...string) cli.Opt

WithAutoEnvBindings enables the feature which can auto-bind env-vars to flags default value.

For example, APP_JUMP_TO_FULL=1 -> Cmd{"jump.to"}.Flag{"full"}.default-value = true. In this case, `APP` is default prefix so the env-var is different than other normal OS env-vars (like HOME, etc.).

You may specify another prefix optionally. For instance, prefix "CT" will cause CT_JUMP_TO_FULL=1 binding to Cmd{"jump.to"}.Flag{"full"}.default-value = true.

Also you can specify the prefix with multiple section just like "CT_ACCOUNT_SERVICE", it will be treated as a normal plain string and concatted with the rest parts, so "CT_ACCOUNT_SERVICE_JUMP_TO_FULL=1" will be bound in.

func WithConfig added in v2.0.9

func WithConfig(conf *cli.Config) cli.Opt

WithConfig allows you passing a *cli.Config object directly.

func WithDontExecuteAction added in v2.0.7

func WithDontExecuteAction(b bool) cli.Opt

WithDontExecuteAction prevents internal exec stage which will invoke the matched command's cli.Cmd.OnAction handler.

If cli.Config.DontExecuteAction is true, cmdr works like classical golang stdlib 'flag', which will stop after parsed without any further actions.

cmdr itself is a parsing-and-executing processor. We will launch a matched command's handlers by default.

func WithDontGroupInHelpScreen added in v2.0.7

func WithDontGroupInHelpScreen(b bool) cli.Opt

func WithExternalLoaders

func WithExternalLoaders(loaders ...cli.Loader) cli.Opt

WithExternalLoaders sets the loaders of external sources, which will be loaded at cmdr's preparing time (xref-building time).

The orders could be referred as:

- constructing cmdr commands system (by your prepareApp) - entering cmdr.Run - cmdr preparing stage

  • build commands and flags xref
  • load and apply envvars if matched
  • load external sources
  • post preparing time

- cmdr parsing stage - cmdr invoking stage - cmdr cleanup stage

Using our loaders repo is a good idea: https://github.com/hedzr/cmdr-loaders

Typical app:

app = cmdr.New(opts...).
	Info("tiny-app", "0.3.1").
	Author("The Example Authors") // .Description(``).Header(``).Footer(``)
	cmdr.WithStore(store.New()), // use an option store explicitly, or a dummy store by default

	cmdr.WithExternalLoaders(
		local.NewConfigFileLoader(), // import "github.com/hedzr/cmdr-loaders/local" to get in advanced external loading features
		local.NewEnvVarLoader(),
	),
)
if err := app.Run(ctx); err != nil {
	logz.ErrorContext(ctx, "Application Error:", "err", err) // stacktrace if in debug mode/build
	os.Exit(app.SuggestRetCode())
}

func WithForceDefaultAction

func WithForceDefaultAction(b bool) cli.Opt

func WithPeripherals added in v2.0.7

func WithPeripherals(peripherals ...basics.Peripheral) cli.Opt

WithPeripherals is a better way to register your server peripherals than WithTasksSetupPeripherals. But the 'better' is less.

import "github.com/hedzr/is/basics"
type Obj struct{}
func (o *Obj) Init(context.Context) *Obj { return o } // initialize itself
func (o *Obj) Close(){...}                            // destory itself
obj := new(Obj)                                       // initialize obj at first
ctx := context.Background()                           //
app := cmdr.New(cmdr.WithPeripherals(obj.Init(ctx))   // and register it to basics.Closers for auto-shutting-down
...

func WithSortInHelpScreen added in v2.0.7

func WithSortInHelpScreen(b bool) cli.Opt

func WithStore

func WithStore(conf store.Store) cli.Opt

WithStore gives a user-defined Store as initial, or by default cmdr makes a dummy Store internally.

So you must have a new Store to be transferred into cmdr if you want integrating cmdr and fully-functional Store. Like this,

	app := prepareApp()
	if err := app.Run(
		cmdr.WithStore(store.New()),        // create a standard Store instead of internal dummyStore
		// cmdr.WithExternalLoaders(
		// 	local.NewConfigFileLoader(),    // import "github.com/hedzr/cmdr-loaders/local" to get in advanced external loading features
		// 	local.NewEnvVarLoader(),
		// ),
		cmdr.WithForceDefaultAction(false), // true for debug in developing time
	); err != nil {
		logz.ErrorContext(ctx, "Application Error:", "err", err)
	}

 func prepareApp() cli.App {
		app = cmdr.New().                   // the minimal app is `cmdr.New()`
			Info("tiny-app", "0.3.1").
			Author("example.com Authors")
	}

func WithTasksAfterRun added in v2.0.7

func WithTasksAfterRun(tasks ...cli.Task) cli.Opt

WithTasksAfterRun installs callbacks after run/invoke stage.

The internal stages are: initial -> preload + xref -> parse -> run/invoke -> post-actions.

func WithTasksBeforeParse

func WithTasksBeforeParse(tasks ...cli.Task) cli.Opt

WithTasksBeforeParse installs callbacks before parsing stage.

The internal stages are: initial -> preload + xref -> parse -> run/invoke -> post-actions.

func WithTasksBeforeRun

func WithTasksBeforeRun(tasks ...cli.Task) cli.Opt

WithTasksBeforeRun installs callbacks before run/invoke stage.

The internal stages are: initial -> preload + xref -> parse -> run/invoke -> post-actions.

The internal stages and user-defined tasks are:

  • initial
  • preload & xref
  • <tasksBeforeParse>
  • parse
  • <tasksParsed>
  • <tasksBeforeRun> ( = tasksAfterParse )
  • exec (run/invoke)
  • <tasksAfterRun>
  • <tasksPostCleanup>
  • basics.closers...Close()

func WithTasksParsed added in v2.0.13

func WithTasksParsed(tasks ...cli.Task) cli.Opt

WithTasksParsed installs callbacks after parsing stage.

The internal stages are: initial -> preload + xref -> parse -> run/invoke -> post-actions.

Another way is disabling cmdr default executing/run/invoke stage by WithDontExecuteAction(true).

func WithTasksPostCleanup added in v2.0.13

func WithTasksPostCleanup(tasks ...cli.Task) cli.Opt

WithTasksPostCleanup install callbacks at cmdr ending.

See the stagings order introduce at WithTasksBeforeRun.

See also WithTasksSetupPeripherals, WithPeripherals.

func WithTasksSetupPeripherals added in v2.0.7

func WithTasksSetupPeripherals(tasks ...cli.Task) cli.Opt

WithTasksSetupPeripherals gives a special chance to setup your server's peripherals (such as database, redis, message queues, or others).

For these server peripherals, a better practices would be initializing them with WithTasksSetupPeripherals and destroying them at WithTasksAfterRun.

Another recommendation is implementing your server peripherals as a basics.Closeable component, and register it with basics.RegisterPeripherals(), and that's it. These objects will be destroyed at cmdr ends (later than WithTasksAfterRun but it's okay).

import "github.com/hedzr/is/basics"
type Obj struct{}
func (o *Obj) Init(context.Context) *Obj { return o } // initialize itself
func (o *Obj) Close(){...}                            // destroy itself
app := cmdr.New(cmdr.WithTasksSetupPeripherals(func(ctx context.Context, cmd cli.Cmd, runner cli.Runner, extras ...any) (err error) {
    obj := new(Obj)
    basics.RegisterPeripheral(obj.Init(ctx))          // initialize obj at first, and register it to basics.Closers for auto-shutting-down
    return
}))
...

func WithUnmatchedAsError

func WithUnmatchedAsError(b bool) cli.Opt

Types

This section is empty.

Directories

Path Synopsis
cli
atoa
Package atoa - converters for any to any
Package atoa - converters for any to any
Package conf are used to store the app-level constants (app name/vaersion) for cmdr and your app.
Package conf are used to store the app-level constants (app name/vaersion) for cmdr and your app.
internal
hs
pkg
dir
Package dir provides a series of directory/file operations
Package dir provides a series of directory/file operations

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL