// Status is a command that allows a user to view their progress in a given // language track. func Status(ctx *cli.Context) { c, err := config.New(ctx.GlobalString("config")) if err != nil { log.Fatal(err) } args := ctx.Args() if len(args) != 1 { fmt.Fprintf(os.Stderr, "Usage: exercism status TRACK_ID") os.Exit(1) } client := api.NewClient(c) trackID := args[0] status, err := client.Status(trackID) if err != nil { if err == api.ErrUnknownTrack { log.Fatalf("There is no track with ID '%s'.", trackID) } else { log.Fatal(err) } } fmt.Println(status) }
// List returns the full list of assignments for a given language func List(ctx *cli.Context) { c, err := config.New(ctx.GlobalString("config")) if err != nil { log.Fatal(err) } args := ctx.Args() if len(args) != 1 { msg := "Usage: exercism list LANGUAGE" log.Fatal(msg) } language := args[0] client := api.NewClient(c) problems, err := client.List(language) if err != nil { if err == api.ErrUnknownLanguage { log.Fatalf("The requested language '%s' is unknown", language) } log.Fatal(err) } for _, p := range problems { fmt.Printf("%s\n", p) } fmt.Printf("\n%s\n\n", msgExplainFetch) }
// Configure stores settings in a JSON file. // If a setting is not passed as an argument, default // values are used. func Configure(ctx *cli.Context) { c, err := config.New(ctx.GlobalString("config")) if err != nil { log.Fatal(err) } key := ctx.String("key") host := ctx.String("host") dir := ctx.String("dir") api := ctx.String("api") if err := c.Update(key, host, dir, api); err != nil { log.Fatalf("Error updating your configuration %s\n", err) } if err := os.MkdirAll(c.Dir, os.ModePerm); err != nil { log.Fatalf("Error creating exercism directory %s\n", err) } if err := c.Write(); err != nil { log.Fatal(err) } fmt.Printf("The configuration has been written to %s\n", c.File) fmt.Printf("Your exercism directory can be found at %s\n", c.Dir) }
// Demo returns one problem for each active track. func Demo(ctx *cli.Context) { c, err := config.New(ctx.GlobalString("config")) if err != nil { log.Fatal(err) } client := api.NewClient(c) problems, err := client.Demo() if err != nil { log.Fatal(err) } if dirOpt := ctx.String("dir"); dirOpt != "" { c.SetDir(dirOpt) } fmt.Printf("Your exercises will be saved at: %s\n", c.Dir) hw := user.NewHomework(problems, c) if err := hw.Save(); err != nil { log.Fatal(err) } hw.Report(user.HWAll) fmt.Println("Next step: choose a language, read the README, and make the test suite pass.") }
// List returns the full list of assignments for a given track. func List(ctx *cli.Context) { c, err := config.New(ctx.GlobalString("config")) if err != nil { log.Fatal(err) } args := ctx.Args() if len(args) != 1 { msg := "Usage: exercism list TRACK_ID" log.Fatal(msg) } trackID := args[0] client := api.NewClient(c) problems, err := client.List(trackID) if err != nil { if err == api.ErrUnknownTrack { log.Fatalf("There is no track with ID '%s'.", trackID) } log.Fatal(err) } for _, p := range problems { fmt.Printf("%s\n", p) } fmt.Printf("\n%s\n\n", msgExplainFetch) }
// Fetch downloads exercism problems and writes them to disk. func Fetch(ctx *cli.Context) { c, err := config.New(ctx.GlobalString("config")) if err != nil { log.Fatal(err) } client := api.NewClient(c) problems, err := client.Fetch(ctx.Args()) if err != nil { log.Fatal(err) } submissionInfo, err := client.Submissions() if err != nil { log.Fatal(err) } if err := setSubmissionState(problems, submissionInfo); err != nil { log.Fatal(err) } hw := user.NewHomework(problems, c) if err := hw.Save(); err != nil { log.Fatal(err) } hw.Summarize(user.HWAll) }
// Tracks lists available tracks. func Tracks(ctx *cli.Context) { c, err := config.New(ctx.GlobalString("config")) if err != nil { log.Fatal(err) } client := api.NewClient(c) tracks, err := client.Tracks() if err != nil { log.Fatal(err) } curr := user.NewCurriculum(tracks) fmt.Println("\nActive language tracks:") curr.Report(user.TrackActive) fmt.Println("\nInactive language tracks:") curr.Report(user.TrackInactive) msg := ` Related commands: exercism fetch (see 'exercism help fetch') exercism list (see 'exercism help list') ` fmt.Println(msg) }
// Debug provides information about the user's environment and configuration. func Debug(ctx *cli.Context) { defer fmt.Printf("\nIf you are having trouble and need to file a GitHub issue (https://github.com/exercism/exercism.io/issues) please include this information (except your API key. Keep that private).\n") client := http.Client{Timeout: 5 * time.Second} fmt.Printf("\n**** Debug Information ****\n") fmt.Printf("Exercism CLI Version: %s\n", ctx.App.Version) rel, err := fetchLatestRelease(client) if err != nil { log.Println("unable to fetch latest release: " + err.Error()) } else { if rel.Version() != ctx.App.Version { defer fmt.Printf("\nA newer version of the CLI (%s) can be downloaded here: %s\n", rel.TagName, rel.Location) } fmt.Printf("Exercism CLI Latest Release: %s\n", rel.Version()) } fmt.Printf("OS/Architecture: %s/%s\n", runtime.GOOS, runtime.GOARCH) fmt.Printf("Build OS/Architecture %s/%s\n", BuildOS, BuildARCH) if BuildARM != "" { fmt.Printf("Build ARMv%s\n", BuildARM) } dir, err := config.Home() if err != nil { log.Fatal(err) } fmt.Printf("Home Dir: %s\n", dir) c, err := config.New(ctx.GlobalString("config")) if err != nil { log.Fatal(err) } configured := true if _, err = os.Stat(c.File); err != nil { if os.IsNotExist(err) { configured = false } else { log.Fatal(err) } } if configured { fmt.Printf("Config file: %s\n", c.File) fmt.Printf("API Key: %s\n", c.APIKey) } else { fmt.Println("Config file: <not configured>") fmt.Println("API Key: <not configured>") } fmt.Printf("API: %s [%s]\n", c.API, pingURL(client, c.API)) fmt.Printf("XAPI: %s [%s]\n", c.XAPI, pingURL(client, c.XAPI)) fmt.Printf("Exercises Directory: %s\n", c.Dir) }
// Unsubmit deletes an iteration from the api. // If no iteration is specified, the most recent iteration // is deleted. func Unsubmit(ctx *cli.Context) { c, err := config.New(ctx.GlobalString("config")) if err != nil { log.Fatal(err) } if !c.IsAuthenticated() { log.Fatal(msgPleaseAuthenticate) } client := api.NewClient(c) if err := client.Unsubmit(); err != nil { log.Fatal(err) } fmt.Println("Your most recent submission was successfully deleted.") }
// Fetch downloads exercism problems and writes them to disk. func Fetch(ctx *cli.Context) { c, err := config.New(ctx.GlobalString("config")) if err != nil { log.Fatal(err) } client := api.NewClient(c) problems, err := client.Fetch(ctx.Args()) if err != nil { log.Fatal(err) } submissionInfo, err := client.Submissions() if err != nil { log.Fatal(err) } if err := setSubmissionState(problems, submissionInfo); err != nil { log.Fatal(err) } dirs, err := filepath.Glob(filepath.Join(c.Dir, "*")) if err != nil { log.Fatal(err) } dirMap := make(map[string]bool) for _, dir := range dirs { dirMap[dir] = true } hw := user.NewHomework(problems, c) if len(ctx.Args()) == 0 { if err := hw.RejectMissingTracks(dirMap); err != nil { log.Fatal(err) } } if err := hw.Save(); err != nil { log.Fatal(err) } hw.Summarize(user.HWAll) }
// Open uses the given track and problem and opens it in the browser. func Open(ctx *cli.Context) { c, err := config.New(ctx.GlobalString("config")) if err != nil { log.Fatal(err) } client := api.NewClient(c) args := ctx.Args() if len(args) != 2 { fmt.Fprintf(os.Stderr, "Usage: exercism open TRACK_ID PROBLEM") os.Exit(1) } trackID := args[0] slug := args[1] submission, err := client.SubmissionURL(trackID, slug) if err != nil { log.Fatal(err) } url := submission.URL // Escape characters are not allowed by cmd/bash. switch runtime.GOOS { case "windows": url = strings.Replace(url, "&", `^&`, -1) default: url = strings.Replace(url, "&", `\&`, -1) } // The command to open the browser is OS-dependent. var cmd *exec.Cmd switch runtime.GOOS { case "darwin": cmd = exec.Command("open", url) case "freebsd", "linux", "netbsd", "openbsd": cmd = exec.Command("xdg-open", url) case "windows": cmd = exec.Command("cmd", "/c", "start", url) } if err := cmd.Run(); err != nil { log.Fatal(err) } }
// Restore returns a user's solved problems. func Restore(ctx *cli.Context) { c, err := config.New(ctx.GlobalString("config")) if err != nil { log.Fatal(err) } client := api.NewClient(c) problems, err := client.Restore() if err != nil { log.Fatal(err) } hw := user.NewHomework(problems, c) if err := hw.Save(); err != nil { log.Fatal(err) } hw.Summarize(user.HWNotSubmitted) }
// Status is a command that allows a user to view their progress in a given // language track. func Status(ctx *cli.Context) { c, err := config.New(ctx.GlobalString("config")) if err != nil { log.Fatal(err) } args := ctx.Args() if len(args) != 1 { log.Fatal("Usage: exercism status TRACK_ID") } client := api.NewClient(c) status, err := client.Status(args[0]) if err != nil { log.Fatal(err) } fmt.Println(status) }
// Unsubmit deletes the most recent submission from the API. func Unsubmit(ctx *cli.Context) { if len(ctx.Args()) > 0 { log.Fatal("\nThe unsubmit command does not take any arguments, it deletes the most recent submission.\n\nTo delete a different submission, you'll need to do it from the website.") } c, err := config.New(ctx.GlobalString("config")) if err != nil { log.Fatal(err) } if !c.IsAuthenticated() { log.Fatal(msgPleaseAuthenticate) } client := api.NewClient(c) if err := client.Unsubmit(); err != nil { log.Fatal(err) } fmt.Println("Your most recent submission was successfully deleted.") }
// Download returns specified iteration with its related problem. func Download(ctx *cli.Context) { c, err := config.New(ctx.GlobalString("config")) if err != nil { log.Fatal(err) } client := api.NewClient(c) args := ctx.Args() if len(args) != 1 { msg := "Usage: exercism download SUBMISSION_ID" log.Fatal(msg) } submission, err := client.Download(args[0]) if err != nil { log.Fatal(err) } path := filepath.Join(c.Dir, "solutions", submission.Username, submission.TrackID, submission.Slug, args[0]) if err := os.MkdirAll(path, 0755); err != nil { log.Fatal(err) } for name, contents := range submission.ProblemFiles { if err := ioutil.WriteFile(fmt.Sprintf("%s/%s", path, name), []byte(contents), 0755); err != nil { log.Fatalf("Unable to write file %s: %s", name, err) } } for name, contents := range submission.SolutionFiles { filename := strings.TrimPrefix(name, strings.ToLower("/"+submission.TrackID+"/"+submission.Slug+"/")) if err := ioutil.WriteFile(fmt.Sprintf("%s/%s", path, filename), []byte(contents), 0755); err != nil { log.Fatalf("Unable to write file %s: %s", name, err) } } fmt.Printf("Successfully downloaded submission.\n\nThe submission can be viewed at:\n %s\n\n", path) }
// Skip allows a user to skip a specific problem. func Skip(ctx *cli.Context) { c, err := config.New(ctx.GlobalString("config")) if err != nil { log.Fatal(err) } args := ctx.Args() if len(args) != 2 { msg := "Usage: exercism skip TRACK_ID PROBLEM" log.Fatal(msg) } var ( trackID = args[0] slug = args[1] ) client := api.NewClient(c) if err := client.Skip(trackID, slug); err != nil { log.Fatal(err) } fmt.Printf("Exercise %q in %q has been skipped.\n", slug, trackID) }
// Skip command allows a user to skip a specific problem func Skip(ctx *cli.Context) { c, err := config.New(ctx.GlobalString("config")) if err != nil { log.Fatal(err) } args := ctx.Args() if len(args) != 2 { msg := "Usage: exercism skip LANGUAGE SLUG" log.Fatal(msg) } var ( language = args[0] slug = args[1] ) client := api.NewClient(c) if err := client.Skip(language, slug); err != nil { log.Fatal(err) } fmt.Printf("Exercise %q in %q has been skipped.\n", slug, language) }
// Submit posts an iteration to the API. func Submit(ctx *cli.Context) { if len(ctx.Args()) == 0 { log.Fatal("Please enter a file name") } c, err := config.New(ctx.GlobalString("config")) if err != nil { log.Fatal(err) } if ctx.GlobalBool("verbose") { log.Printf("Exercises dir: %s", c.Dir) dir, err := os.Getwd() if err != nil { log.Printf("Unable to get current working directory - %s", err) } else { log.Printf("Current dir: %s", dir) } } if !c.IsAuthenticated() { log.Fatal(msgPleaseAuthenticate) } dir, err := filepath.EvalSymlinks(c.Dir) if err != nil { log.Fatal(err) } if ctx.GlobalBool("verbose") { log.Printf("eval symlinks (dir): %s", dir) } files := []string{} for _, filename := range ctx.Args() { if ctx.GlobalBool("verbose") { log.Printf("file name: %s", filename) } if isTest(filename) && !ctx.Bool("test") { log.Fatal("You're trying to submit a test file. If this is really what " + "you want, please pass the --test flag to exercism submit.") } if isREADME(filename) { log.Fatal("You cannot submit the README as a solution.") } if paths.IsDir(filename) { log.Fatal("Please specify each file that should be submitted, e.g. `exercism submit file1 file2 file3`.") } file, err := filepath.Abs(filename) if err != nil { log.Fatal(err) } if ctx.GlobalBool("verbose") { log.Printf("absolute path: %s", file) } file, err = filepath.EvalSymlinks(file) if err != nil { log.Fatal(err) } if ctx.GlobalBool("verbose") { log.Printf("eval symlinks (file): %s", file) } files = append(files, file) } iteration, err := api.NewIteration(dir, files) if err != nil { log.Fatalf("Unable to submit - %s", err) } iteration.Key = c.APIKey iteration.Comment = ctx.String("comment") client := api.NewClient(c) submission, err := client.Submit(iteration) if err != nil { log.Fatal(err) } fmt.Printf("%s - %s\n%s\n\n", submission.Language, submission.Name, submission.URL) }
// Debug provides information about the user's environment and configuration. func Debug(ctx *cli.Context) { defer fmt.Printf("\nIf you are having trouble and need to file a GitHub issue (https://github.com/exercism/exercism.io/issues) please include this information (except your API key. Keep that private).\n") client := &http.Client{Timeout: 20 * time.Second} fmt.Printf("\n**** Debug Information ****\n") fmt.Printf("Exercism CLI Version: %s\n", ctx.App.Version) rel, err := fetchLatestRelease(*client) if err != nil { log.Println("unable to fetch latest release: " + err.Error()) } else { if rel.Version() != ctx.App.Version { defer fmt.Printf("\nA newer version of the CLI (%s) can be downloaded here: %s\n", rel.TagName, rel.Location) } fmt.Printf("Exercism CLI Latest Release: %s\n", rel.Version()) } fmt.Printf("OS/Architecture: %s/%s\n", runtime.GOOS, runtime.GOARCH) fmt.Printf("Build OS/Architecture %s/%s\n", BuildOS, BuildARCH) if BuildARM != "" { fmt.Printf("Build ARMv%s\n", BuildARM) } fmt.Printf("Home Dir: %s\n", paths.Home) c, err := config.New(ctx.GlobalString("config")) if err != nil { log.Fatal(err) } configured := true if _, err = os.Stat(c.File); err != nil { if os.IsNotExist(err) { configured = false } else { log.Fatal(err) } } if configured { fmt.Printf("Config file: %s\n", c.File) fmt.Printf("API Key: %s\n", c.APIKey) } else { fmt.Println("Config file: <not configured>") fmt.Println("API Key: <not configured>") } fmt.Printf("Exercises Directory: %s\n", c.Dir) fmt.Println("Testing API endpoints reachability") endpoints := map[string]string{ "API": c.API, "XAPI": c.XAPI, "GitHub API": "https://api.github.com/", } var wg sync.WaitGroup results := make(chan pingResult) defer close(results) wg.Add(len(endpoints)) for service, url := range endpoints { go func(service, url string) { now := time.Now() res, err := client.Get(url) delta := time.Since(now) if err != nil { results <- pingResult{ URL: url, Service: service, Status: err.Error(), Latency: delta, } return } defer res.Body.Close() results <- pingResult{ URL: url, Service: service, Status: "connected", Latency: delta, } }(service, url) } go func() { for r := range results { fmt.Printf( "\t* %s: %s [%s] %s\n", r.Service, r.URL, r.Status, r.Latency, ) wg.Done() } }() wg.Wait() }
// Submit posts an iteration to the API. func Submit(ctx *cli.Context) { if len(ctx.Args()) == 0 { log.Fatal("Please enter a file name") } c, err := config.New(ctx.GlobalString("config")) if err != nil { log.Fatal(err) } if ctx.GlobalBool("verbose") { log.Printf("Exercises dir: %s", c.Dir) dir, err := os.Getwd() if err != nil { log.Printf("Unable to get current working directory - %s", err) } else { log.Printf("Current dir: %s", dir) } } if !c.IsAuthenticated() { log.Fatal(msgPleaseAuthenticate) } dir, err := filepath.EvalSymlinks(c.Dir) if err != nil { log.Fatal(err) } if ctx.GlobalBool("verbose") { log.Printf("eval symlinks (dir): %s", dir) } files := []string{} for _, filename := range ctx.Args() { if ctx.GlobalBool("verbose") { log.Printf("file name: %s", filename) } if isTest(filename) && !ctx.Bool("test") { log.Fatal("You're trying to submit a test file. If this is really what " + "you want, please pass the --test flag to exercism submit.") } if isREADME(filename) { log.Fatal("You cannot submit the README as a solution.") } file, err := filepath.Abs(filename) if err != nil { log.Fatal(err) } if ctx.GlobalBool("verbose") { log.Printf("absolute path: %s", file) } file, err = filepath.EvalSymlinks(file) if err != nil { log.Fatal(err) } if ctx.GlobalBool("verbose") { log.Printf("eval symlinks (file): %s", file) } files = append(files, file) } iteration, err := api.NewIteration(dir, files) if err != nil { log.Fatalf("Unable to submit - %s", err) } iteration.Key = c.APIKey iteration.Comment = ctx.String("comment") client := api.NewClient(c) submission, err := client.Submit(iteration) if err != nil { log.Fatal(err) } msg := ` Submitted %s in %s. Your submission can be found online at %s ` if submission.Iteration == 1 { msg += ` To get the next exercise, run "exercism fetch" again. ` rand.Seed(time.Now().UTC().UnixNano()) phrases := []string{ "For bonus points", "Don't stop now: The fun's just begun", "Some tips to continue", } msg += fmt.Sprintf("\n## %s\n", phrases[rand.Intn(len(phrases))]) msg += tips } fmt.Printf(msg, submission.Name, submission.Language, submission.URL) }
// Submit posts an iteration to the API. func Submit(ctx *cli.Context) { if len(ctx.Args()) == 0 { log.Fatal("Please enter a file name") } c, err := config.New(ctx.GlobalString("config")) if err != nil { log.Fatal(err) } if ctx.GlobalBool("verbose") { log.Printf("Exercises dir: %s", c.Dir) dir, err := os.Getwd() if err != nil { log.Printf("Unable to get current working directory - %s", err) } else { log.Printf("Current dir: %s", dir) } } if !c.IsAuthenticated() { log.Fatal(msgPleaseAuthenticate) } dir, err := filepath.EvalSymlinks(c.Dir) if err != nil { log.Fatal(err) } if ctx.GlobalBool("verbose") { log.Printf("eval symlinks (dir): %s", dir) } files := []string{} for _, filename := range ctx.Args() { if ctx.GlobalBool("verbose") { log.Printf("file name: %s", filename) } if isTest(filename) && !ctx.Bool("test") { log.Fatal("You're trying to submit a test file. If this is really what " + "you want, please pass the --test flag to exercism submit.") } file, err := filepath.Abs(filename) if err != nil { log.Fatal(err) } if ctx.GlobalBool("verbose") { log.Printf("absolute path: %s", file) } file, err = filepath.EvalSymlinks(file) if err != nil { log.Fatal(err) } if ctx.GlobalBool("verbose") { log.Printf("eval symlinks (file): %s", file) } files = append(files, file) } iteration, err := api.NewIteration(dir, files) if err != nil { log.Fatalf("Unable to submit - %s", err) } iteration.Key = c.APIKey client := api.NewClient(c) submission, err := client.Submit(iteration) if err != nil { log.Fatal(err) } msg := ` Submitted %s in %s. Your submission can be found online at %s To get the next exercise, run "exercism fetch" again. ` fmt.Printf(msg, submission.Name, submission.Language, submission.URL) }