This is how I decided to organize files and folders. I wanted everything to be located where it would intuitively be looked for.
The version is kept in pkg/version/version.go and then piped into the Makefile and imported into the client to print. This isn't the only way to do it, but seemed reasonable.
- The user first interacts with the scif client, defined in package "main" under cmd/scif. The names of files there correspond with commands invoked.
- The main entrypoints instantiate a client in the client package, located under client/pkg. The files are named according to subcommand. It should be fairly easy to hop from "install.go" in the command folder to the functions it calls, also in "install.go" but in the pkg/client folder.
- The environment is sniffed during setup of the client instance (well, it's a struct with functions) and this is done and defaults set in defaults.go. If you export an environment variable to be found, you can run the client with --debug to see if it's found.
cmd/scif is the entrypoint command for the application. The file main.go has the main client, and the other *.go files in the folder correspond with subcommands (e.g., "run.go"). Flags that are global are in flags.go, and environment.go has environment parsing. Generally, you can find what you are looking for based on the naming, despite the fact that all these files are in shared "package main" and get compiled together.
pkg/client is logically a package for the client functions. These are the functions that are called from the entrypoint client, the base of which is defined in pkg/client/client.go. The execution first creates a struct that is going to hold client variables:
type ScifClient struct {
Base string // /scif is the overall base
Data string // <Base>/data is the data base
Apps string // <Base>/apps is the apps base
...
ShellCmd string // default shell
}and then we create an initialization function that is going to help define some of those variables, and do other setup tasks. Notice how it returns an instantiated version of ScifClient:
func NewScifClient() *ScifClient {
base := getenv("SCIF_BASE", getStringDefault("BASE"))
scifApps := getenvNamespace("SCIF_APP")
...
// Instantiate the client
client := &ScifClient{Base: base,
Data: data,
Apps: apps,
ShellCmd: shell,
EntryPoint: entrylist,
EntryFolder: entryfolder,
allowAppend: allowAppend,
appendPaths: scifAppendPaths,
scifApps: scifApps}
// Setup includes loading a scif, if found at base
client.Setup()
return client
}And thus, this object (?) is available to the user (with variables defined) as "Scif":
// provide client to user as "Scif"
var Scif ScifClient = *NewScifClient()Thus, when we call a function in pkg/client from an entrypoint in cmd/scif
we call the <package>/<function> directly, and it's usually the case they are found in matching files (e.g., install.go
and install.go in each directory). The example below shows calling Install in the client package after
parsing input arguments for a recipe, additional arguments, and a boolean:
client.Install(recipe, args, !readonly)Then in the package "client" install.go we might check that the recipe exist, and create an instance of the ScifRecipe struct that has all the helper functions attached. If we want to load a recipe that is provided, we create the client like this:
// Create the client, load the recipe
cli := ScifClient{}.Load(recipe, apps)Otherwise we don't call load (and could call it later, if desired):
// Create the client
cli := ScifClient{}Either way, after we have loaded, we can further call functions that are owned by the client.
// install Base folders
cli.installBase()
cli.installApps(apps)We add functions to the ScifRecipe like this:
func (client ScifClient) Execute() {
logger.Debugf("Execute() here")
fmt.Println("The base is at %s", Scif.Base)
}And notice how we reference the variables that have been initialized via Scif.Base.