├── .gitignore ├── .vscode └── settings.json ├── src ├── common │ ├── types.go │ ├── flags.go │ ├── tests │ │ └── common.go │ ├── logger.go │ ├── constants.go │ └── helpers │ │ └── random.go └── commands │ ├── shuffle.go │ ├── pick.go │ ├── string.go │ ├── id.go │ ├── shuffle_test.go │ ├── string_test.go │ └── pick_test.go ├── .github └── workflows │ ├── ci.yml │ ├── test-dev.yml │ └── test-master.yml ├── main.go ├── go.mod ├── readme.md ├── LICENSE └── go.sum /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .src -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "cSpell.words": [ 3 | "CSPRNG" 4 | ] 5 | } -------------------------------------------------------------------------------- /src/common/types.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | func ToArr[F, T any](from []F) []T { 4 | results := make([]T, len(from)) 5 | for i, value := range from { 6 | results[i] = any(value).(T) 7 | } 8 | 9 | return results 10 | } 11 | -------------------------------------------------------------------------------- /src/common/flags.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import "github.com/urfave/cli/v2" 4 | 5 | var CountFlag = &cli.IntFlag{ 6 | Name: "count", 7 | Aliases: []string{"c"}, 8 | Value: 1, 9 | Usage: "Number of random strings to display", 10 | } 11 | -------------------------------------------------------------------------------- /src/common/tests/common.go: -------------------------------------------------------------------------------- 1 | package tests 2 | 3 | import ( 4 | "bytes" 5 | "log" 6 | 7 | "github.com/AmrSaber/random/v3/src/common" 8 | ) 9 | 10 | func SetupTest() (*bytes.Buffer, func()) { 11 | var buf bytes.Buffer 12 | oldStd := common.Std 13 | common.Std = log.New(&buf, "", 0) 14 | return &buf, func() { common.Std = oldStd } 15 | } 16 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - master 7 | - dev 8 | 9 | jobs: 10 | test: 11 | runs-on: ubuntu-latest 12 | container: golang:1-alpine 13 | 14 | steps: 15 | - uses: actions/checkout@v2 16 | 17 | - name: Install Dependencies 18 | run: go mod download 19 | 20 | - name: Lint 21 | run: go test ./... -v 22 | -------------------------------------------------------------------------------- /.github/workflows/test-dev.yml: -------------------------------------------------------------------------------- 1 | name: Test Dev 2 | 3 | on: 4 | push: 5 | branches: 6 | - dev 7 | 8 | jobs: 9 | run-tests: 10 | name: Run tests 11 | runs-on: ubuntu-latest 12 | container: golang:1-alpine 13 | 14 | steps: 15 | - uses: actions/checkout@v2 16 | 17 | - name: Install Dependencies 18 | run: go mod download 19 | 20 | - name: Run Tests 21 | run: go test ./... -v 22 | -------------------------------------------------------------------------------- /.github/workflows/test-master.yml: -------------------------------------------------------------------------------- 1 | name: Test Master 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | jobs: 9 | run-tests: 10 | name: Run tests on master 11 | runs-on: ubuntu-latest 12 | container: golang:1-alpine 13 | 14 | steps: 15 | - uses: actions/checkout@v2 16 | 17 | - name: Install Dependencies 18 | run: go mod download 19 | 20 | - name: Run Tests 21 | run: go test ./... -v 22 | -------------------------------------------------------------------------------- /src/common/logger.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "log" 7 | "os" 8 | 9 | "github.com/fatih/color" 10 | ) 11 | 12 | var Err = color.New(color.FgRed) 13 | var Std = log.New(os.Stdout, "", 0) 14 | 15 | func Fail(message any) { 16 | Err.Fprint(os.Stderr, fmt.Sprintf("%s\n", message)) 17 | os.Exit(1) 18 | } 19 | 20 | func ColorStr(str string, attributes ...color.Attribute) string { 21 | var buffer bytes.Buffer 22 | color.New(attributes...).Fprint(&buffer, str) 23 | return buffer.String() 24 | } 25 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/AmrSaber/random/v3/src/commands" 7 | "github.com/AmrSaber/random/v3/src/common" 8 | 9 | "github.com/urfave/cli/v2" 10 | ) 11 | 12 | func main() { 13 | app := &cli.App{ 14 | Name: "random", 15 | Description: "CLI tool to generate random data", 16 | Commands: []*cli.Command{commands.StringCommand, commands.ShuffleCommand, commands.PickCommand, commands.IdCommand}, 17 | } 18 | 19 | // Run CLI 20 | if err := app.Run(os.Args); err != nil { 21 | common.Fail(err) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/AmrSaber/random/v3 2 | 3 | go 1.24.2 4 | 5 | require ( 6 | github.com/fatih/color v1.18.0 7 | github.com/google/uuid v1.6.0 8 | github.com/matoous/go-nanoid/v2 v2.1.0 9 | github.com/urfave/cli/v2 v2.27.5 10 | ) 11 | 12 | require ( 13 | github.com/cpuguy83/go-md2man/v2 v2.0.5 // indirect 14 | github.com/mattn/go-colorable v0.1.13 // indirect 15 | github.com/mattn/go-isatty v0.0.20 // indirect 16 | github.com/russross/blackfriday/v2 v2.1.0 // indirect 17 | github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect 18 | golang.org/x/sys v0.25.0 // indirect 19 | ) 20 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Random Generator CLI 2 | 3 | > Old package published on npm has been removed since it was broken and I don't intend to fix it. 4 | 5 | Generates and prints a secure random string (and other types) to terminal. 6 | 7 | ## Install 8 | Install `go` then run 9 | ```bash 10 | go install github.com/AmrSaber/random/v3@latest 11 | ``` 12 | 13 | Then you can access the command as `random`. 14 | 15 | ## Usage 16 | After installation, use `random `, you can use `random -h` to show help message related to that command, or use `random -h` to list all the available commands and options. 17 | 18 | ### Commands 19 | Available commands are [`string`, `pick`, `shuffle`, `id`]. 20 | 21 | ## Feature requests and bug reports 22 | If you have a bug or have a goode idea for a new feature, please [open an issue](https://github.com/AmrSaber/rand-string/issues). 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Amr Saber 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/commands/shuffle.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/AmrSaber/random/v3/src/common" 7 | "github.com/AmrSaber/random/v3/src/common/helpers" 8 | 9 | "github.com/urfave/cli/v2" 10 | ) 11 | 12 | var ShuffleCommand = &cli.Command{ 13 | Name: "shuffle", 14 | Description: "Shuffles the given items", 15 | 16 | Flags: []cli.Flag{ 17 | common.CountFlag, 18 | 19 | &cli.StringFlag{ 20 | Name: "delimiter", 21 | Aliases: []string{"d"}, 22 | Usage: "Delimiter for the shuffled output", 23 | Value: common.DEFAULT_DELIMITER, 24 | }, 25 | }, 26 | 27 | Action: func(ctx *cli.Context) error { 28 | count := ctx.Int("count") 29 | delimiter := ctx.String("delimiter") 30 | values := ctx.Args().Slice() 31 | 32 | if len(values) == 0 { 33 | return nil 34 | } 35 | 36 | randomShuffles := make([]string, 0, count) 37 | 38 | for range count { 39 | shuffledItems := helpers.Shuffle(values) 40 | randomShuffles = append(randomShuffles, strings.Join(shuffledItems, delimiter)) 41 | } 42 | 43 | // Print the generated random strings 44 | common.Std.Println(strings.Join(randomShuffles, "\n")) 45 | 46 | return nil 47 | }, 48 | } 49 | -------------------------------------------------------------------------------- /src/common/constants.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | // Boolean type constants 4 | const ( 5 | BOOLEAN_TYPE_TRUE_FALSE string = "true_false" 6 | BOOLEAN_TYPE_NUMERIC string = "numeric" 7 | BOOLEAN_TYPE_YES_NO string = "yes_no" 8 | ) 9 | 10 | // String type constants 11 | const ( 12 | STRING_TYPE_HEX string = "hex" 13 | STRING_TYPE_ASCII string = "ascii" 14 | STRING_TYPE_BASE_64 string = "base64" 15 | STRING_TYPE_NUMBERS string = "numbers" 16 | STRING_TYPE_LETTERS string = "letters" 17 | STRING_TYPE_EXTENDED string = "extended" 18 | ) 19 | 20 | var STRING_TYPES = []string{ 21 | STRING_TYPE_HEX, 22 | STRING_TYPE_ASCII, 23 | STRING_TYPE_NUMBERS, 24 | STRING_TYPE_LETTERS, 25 | STRING_TYPE_EXTENDED, 26 | STRING_TYPE_BASE_64, 27 | } 28 | 29 | // Possible string values 30 | var ( 31 | ASCII_LETTERS = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ") 32 | NUMBERS = []rune("0123456789") 33 | HEX_DIGITS = []rune("0123456789abcdef") 34 | ) 35 | 36 | const DEFAULT_DELIMITER = " " 37 | const DEFAULT_STRING_LENGTH = 20 38 | 39 | const ( 40 | ID_TYPE_UUID4 = "uuidv4" 41 | ID_TYPE_UUID7 = "uuidv7" 42 | ID_TYPE_NANO = "nanoid" 43 | ) 44 | 45 | var ID_TYPES = []string{ 46 | ID_TYPE_UUID4, 47 | ID_TYPE_UUID7, 48 | ID_TYPE_NANO, 49 | } 50 | -------------------------------------------------------------------------------- /src/commands/pick.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/AmrSaber/random/v3/src/common" 7 | "github.com/AmrSaber/random/v3/src/common/helpers" 8 | 9 | "github.com/urfave/cli/v2" 10 | ) 11 | 12 | var PickCommand = &cli.Command{ 13 | Name: "pick", 14 | Description: "Picks and print a random item from the given items", 15 | 16 | Flags: []cli.Flag{ 17 | common.CountFlag, 18 | 19 | &cli.StringFlag{ 20 | Name: "delimiter", 21 | Aliases: []string{"d"}, 22 | Usage: "Delimiter for the shuffled output", 23 | Value: common.DEFAULT_DELIMITER, 24 | }, 25 | 26 | &cli.IntFlag{ 27 | Name: "number", 28 | Aliases: []string{"n"}, 29 | Usage: "Number of items to pick", 30 | Value: 1, 31 | }, 32 | }, 33 | 34 | Action: func(ctx *cli.Context) error { 35 | count := ctx.Int("count") 36 | delimiter := ctx.String("delimiter") 37 | number := ctx.Int("number") 38 | values := ctx.Args().Slice() 39 | 40 | if len(values) == 0 { 41 | return nil 42 | } 43 | 44 | randomChoices := make([]string, 0, count) 45 | 46 | for range count { 47 | shuffledItems := helpers.Shuffle(values) 48 | chosenItems := shuffledItems[:number] 49 | randomChoices = append(randomChoices, strings.Join(chosenItems, delimiter)) 50 | } 51 | 52 | common.Std.Println(strings.Join(randomChoices, "\n")) 53 | 54 | return nil 55 | }, 56 | } 57 | -------------------------------------------------------------------------------- /src/commands/string.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "fmt" 5 | "slices" 6 | "strings" 7 | 8 | "github.com/AmrSaber/random/v3/src/common" 9 | "github.com/AmrSaber/random/v3/src/common/helpers" 10 | 11 | "github.com/urfave/cli/v2" 12 | ) 13 | 14 | var StringCommand = &cli.Command{ 15 | Name: "string", 16 | Aliases: []string{"str"}, 17 | Description: "Prints a random string", 18 | 19 | Flags: []cli.Flag{ 20 | common.CountFlag, 21 | 22 | &cli.IntFlag{ 23 | Name: "length", 24 | Aliases: []string{"l"}, 25 | Usage: "Length of the random string", 26 | Value: common.DEFAULT_STRING_LENGTH, 27 | }, 28 | 29 | &cli.StringFlag{ 30 | Name: "type", 31 | Aliases: []string{"t"}, 32 | Usage: fmt.Sprintf("Type of the random string, must be one of [%s]", strings.Join(common.STRING_TYPES, ", ")), 33 | Value: common.STRING_TYPE_ASCII, 34 | Action: func(ctx *cli.Context, s string) error { 35 | if !slices.Contains(common.STRING_TYPES, s) { 36 | return fmt.Errorf("invalid string type %q", s) 37 | } 38 | 39 | return nil 40 | }, 41 | }, 42 | }, 43 | 44 | Action: func(ctx *cli.Context) error { 45 | count := ctx.Int("count") 46 | length := ctx.Int("length") 47 | strType := ctx.String("type") 48 | 49 | randomStrings := make([]string, 0, count) 50 | 51 | for range count { 52 | randomStrings = append(randomStrings, helpers.GetRandomString(strType, length)) 53 | } 54 | 55 | // Print the generated random strings 56 | common.Std.Println(strings.Join(randomStrings, "\n")) 57 | 58 | return nil 59 | }, 60 | } 61 | -------------------------------------------------------------------------------- /src/commands/id.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "fmt" 5 | "slices" 6 | "strings" 7 | 8 | "github.com/AmrSaber/random/v3/src/common" 9 | 10 | "github.com/google/uuid" 11 | gonanoid "github.com/matoous/go-nanoid/v2" 12 | "github.com/urfave/cli/v2" 13 | ) 14 | 15 | var IdCommand = &cli.Command{ 16 | Name: "id", 17 | Description: "Prints a random ID", 18 | 19 | Flags: []cli.Flag{ 20 | common.CountFlag, 21 | 22 | &cli.StringFlag{ 23 | Name: "type", 24 | Aliases: []string{"t"}, 25 | Usage: fmt.Sprintf("Type of the id, must be one of [%s]", strings.Join(common.ID_TYPES, ", ")), 26 | Value: common.ID_TYPE_UUID4, 27 | Action: func(ctx *cli.Context, s string) error { 28 | if !slices.Contains(common.ID_TYPES, s) { 29 | return fmt.Errorf("invalid id type %q", s) 30 | } 31 | 32 | return nil 33 | }, 34 | }, 35 | }, 36 | 37 | Action: func(ctx *cli.Context) error { 38 | count := ctx.Int("count") 39 | idType := ctx.String("type") 40 | 41 | ids := make([]string, 0, count) 42 | 43 | for range count { 44 | var id string 45 | var err error 46 | 47 | if idType == common.ID_TYPE_UUID4 { 48 | id = uuid.New().String() 49 | } 50 | 51 | if idType == common.ID_TYPE_UUID7 { 52 | var _id uuid.UUID 53 | _id, err = uuid.NewV7() 54 | 55 | id = _id.String() 56 | 57 | } 58 | 59 | if idType == common.ID_TYPE_NANO { 60 | id, err = gonanoid.New() 61 | } 62 | 63 | if err != nil { 64 | return fmt.Errorf("error generating nanoid: %w", err) 65 | } 66 | 67 | ids = append(ids, id) 68 | } 69 | 70 | // Print the generated random ids 71 | common.Std.Println(strings.Join(ids, "\n")) 72 | 73 | return nil 74 | }, 75 | } 76 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/cpuguy83/go-md2man/v2 v2.0.5 h1:ZtcqGrnekaHpVLArFSe4HK5DoKx1T0rq2DwVB0alcyc= 2 | github.com/cpuguy83/go-md2man/v2 v2.0.5/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= 3 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 4 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 5 | github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= 6 | github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= 7 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 8 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 9 | github.com/matoous/go-nanoid/v2 v2.1.0 h1:P64+dmq21hhWdtvZfEAofnvJULaRR1Yib0+PnU669bE= 10 | github.com/matoous/go-nanoid/v2 v2.1.0/go.mod h1:KlbGNQ+FhrUNIHUxZdL63t7tl4LaPkZNpUULS8H4uVM= 11 | github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= 12 | github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= 13 | github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= 14 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 15 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 16 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 17 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 18 | github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= 19 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 20 | github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= 21 | github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 22 | github.com/urfave/cli/v2 v2.27.5 h1:WoHEJLdsXr6dDWoJgMq/CboDmyY/8HMMH1fTECbih+w= 23 | github.com/urfave/cli/v2 v2.27.5/go.mod h1:3Sevf16NykTbInEnD0yKkjDAeZDS0A6bzhBH5hrMvTQ= 24 | github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4= 25 | github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM= 26 | golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 27 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 28 | golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34= 29 | golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 30 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 31 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 32 | -------------------------------------------------------------------------------- /src/commands/shuffle_test.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "slices" 5 | "strings" 6 | "testing" 7 | 8 | "github.com/AmrSaber/random/v3/src/common" 9 | "github.com/AmrSaber/random/v3/src/common/tests" 10 | 11 | "github.com/urfave/cli/v2" 12 | ) 13 | 14 | func TestShuffleCommand_Basic(t *testing.T) { 15 | buf, cleanup := tests.SetupTest() 16 | defer cleanup() 17 | 18 | app := &cli.App{Commands: []*cli.Command{ShuffleCommand}} 19 | input := []string{"a", "b", "c"} 20 | args := append([]string{"random", "shuffle"}, input...) 21 | 22 | if err := app.Run(args); err != nil { 23 | t.Errorf("expected no error, got %v", err) 24 | } 25 | 26 | output := strings.TrimSpace(buf.String()) 27 | items := strings.Split(output, common.DEFAULT_DELIMITER) 28 | if len(items) != len(input) { 29 | t.Errorf("expected %d items, got %d", len(input), len(items)) 30 | } 31 | 32 | // Check all input items are present in output 33 | for _, item := range input { 34 | if !slices.Contains(items, item) { 35 | t.Errorf("item %q not found in output", item) 36 | } 37 | } 38 | } 39 | 40 | func TestShuffleCommand_Count(t *testing.T) { 41 | buf, cleanup := tests.SetupTest() 42 | defer cleanup() 43 | 44 | app := &cli.App{Commands: []*cli.Command{ShuffleCommand}} 45 | args := []string{"random", "shuffle", "--count", "3", "a", "b", "c"} 46 | 47 | if err := app.Run(args); err != nil { 48 | t.Errorf("expected no error, got %v", err) 49 | } 50 | 51 | lines := strings.Split(strings.TrimSpace(buf.String()), "\n") 52 | if len(lines) != 3 { 53 | t.Errorf("expected 3 lines, got %d", len(lines)) 54 | } 55 | 56 | // Each line should contain all input items 57 | for _, line := range lines { 58 | items := strings.Split(line, common.DEFAULT_DELIMITER) 59 | if len(items) != 3 { 60 | t.Errorf("expected 3 items per line, got %d", len(items)) 61 | } 62 | } 63 | } 64 | 65 | func TestShuffleCommand_Delimiter(t *testing.T) { 66 | buf, cleanup := tests.SetupTest() 67 | defer cleanup() 68 | 69 | app := &cli.App{Commands: []*cli.Command{ShuffleCommand}} 70 | args := []string{"random", "shuffle", "--delimiter", ", ", "a", "b", "c"} 71 | 72 | if err := app.Run(args); err != nil { 73 | t.Errorf("expected no error, got %v", err) 74 | } 75 | 76 | output := strings.TrimSpace(buf.String()) 77 | items := strings.Split(output, ", ") 78 | if len(items) != 3 { 79 | t.Errorf("expected 3 items, got %d", len(items)) 80 | } 81 | } 82 | 83 | func TestShuffleCommand_EmptyInput(t *testing.T) { 84 | buf, cleanup := tests.SetupTest() 85 | defer cleanup() 86 | 87 | app := &cli.App{Commands: []*cli.Command{ShuffleCommand}} 88 | args := []string{"random", "shuffle"} 89 | 90 | if err := app.Run(args); err != nil { 91 | t.Errorf("expected no error, got %v", err) 92 | } 93 | 94 | output := strings.TrimSpace(buf.String()) 95 | if output != "" { 96 | t.Errorf("expected empty output, got '%s'", output) 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/commands/string_test.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "strings" 5 | "testing" 6 | 7 | "github.com/AmrSaber/random/v3/src/common" 8 | "github.com/AmrSaber/random/v3/src/common/tests" 9 | 10 | "github.com/urfave/cli/v2" 11 | ) 12 | 13 | func TestStringCommand_Basic(t *testing.T) { 14 | buf, cleanup := tests.SetupTest() 15 | defer cleanup() 16 | 17 | app := &cli.App{Commands: []*cli.Command{StringCommand}} 18 | args := []string{"random", "string"} 19 | 20 | if err := app.Run(args); err != nil { 21 | t.Errorf("expected no error, got %v", err) 22 | } 23 | 24 | output := strings.TrimSpace(buf.String()) 25 | if len(output) != common.DEFAULT_STRING_LENGTH { 26 | t.Errorf("expected string length %d, got %d", common.DEFAULT_STRING_LENGTH, len(output)) 27 | } 28 | } 29 | 30 | func TestStringCommand_Length(t *testing.T) { 31 | buf, cleanup := tests.SetupTest() 32 | defer cleanup() 33 | 34 | app := &cli.App{Commands: []*cli.Command{StringCommand}} 35 | args := []string{"random", "string", "--length", "10"} 36 | 37 | if err := app.Run(args); err != nil { 38 | t.Errorf("expected no error, got %v", err) 39 | } 40 | 41 | output := strings.TrimSpace(buf.String()) 42 | if len(output) != 10 { 43 | t.Errorf("expected string length 10, got %d", len(output)) 44 | } 45 | } 46 | 47 | func TestStringCommand_Count(t *testing.T) { 48 | buf, cleanup := tests.SetupTest() 49 | defer cleanup() 50 | 51 | app := &cli.App{Commands: []*cli.Command{StringCommand}} 52 | args := []string{"random", "string", "--count", "3"} 53 | 54 | if err := app.Run(args); err != nil { 55 | t.Errorf("expected no error, got %v", err) 56 | } 57 | 58 | lines := strings.Split(strings.TrimSpace(buf.String()), "\n") 59 | if len(lines) != 3 { 60 | t.Errorf("expected 3 lines, got %d", len(lines)) 61 | } 62 | 63 | for _, line := range lines { 64 | if len(line) != common.DEFAULT_STRING_LENGTH { 65 | t.Errorf("expected string length %d, got %d", common.DEFAULT_STRING_LENGTH, len(line)) 66 | } 67 | } 68 | } 69 | 70 | func TestStringCommand_InvalidType(t *testing.T) { 71 | _, cleanup := tests.SetupTest() 72 | defer cleanup() 73 | 74 | app := &cli.App{Commands: []*cli.Command{StringCommand}} 75 | args := []string{"random", "string", "--type", "invalid"} 76 | 77 | if err := app.Run(args); err == nil { 78 | t.Error("expected error for invalid type, got nil") 79 | } 80 | } 81 | 82 | func TestStringCommand_ValidType(t *testing.T) { 83 | buf, cleanup := tests.SetupTest() 84 | defer cleanup() 85 | 86 | app := &cli.App{Commands: []*cli.Command{StringCommand}} 87 | args := []string{"random", "string", "--type", "hex"} 88 | 89 | if err := app.Run(args); err != nil { 90 | t.Errorf("expected no error, got %v", err) 91 | } 92 | 93 | output := strings.TrimSpace(buf.String()) 94 | if len(output) != common.DEFAULT_STRING_LENGTH { 95 | t.Errorf("expected string length %d, got %d", common.DEFAULT_STRING_LENGTH, len(output)) 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/commands/pick_test.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "slices" 5 | "strings" 6 | "testing" 7 | 8 | "github.com/AmrSaber/random/v3/src/common" 9 | "github.com/AmrSaber/random/v3/src/common/tests" 10 | 11 | "github.com/urfave/cli/v2" 12 | ) 13 | 14 | func TestPickCommand_Basic(t *testing.T) { 15 | buf, cleanup := tests.SetupTest() 16 | defer cleanup() 17 | 18 | app := &cli.App{Commands: []*cli.Command{PickCommand}} 19 | args := []string{"test-app", "pick", "a", "b", "c"} 20 | 21 | if err := app.Run(args); err != nil { 22 | t.Errorf("expected no error, got %v", err) 23 | } 24 | 25 | output := strings.TrimSpace(buf.String()) 26 | if !slices.Contains([]string{"a", "b", "c"}, output) { 27 | t.Errorf("output '%s' is not one of the input items", output) 28 | } 29 | } 30 | 31 | func TestPickCommand_Count(t *testing.T) { 32 | buf, cleanup := tests.SetupTest() 33 | defer cleanup() 34 | 35 | app := &cli.App{Commands: []*cli.Command{PickCommand}} 36 | args := []string{"test-app", "pick", "--count", "3", "a", "b", "c"} 37 | 38 | if err := app.Run(args); err != nil { 39 | t.Errorf("expected no error, got %v", err) 40 | } 41 | 42 | lines := strings.Split(strings.TrimSpace(buf.String()), "\n") 43 | if len(lines) != 3 { 44 | t.Errorf("expected 3 lines, got %d", len(lines)) 45 | } 46 | } 47 | 48 | func TestPickCommand_Number(t *testing.T) { 49 | buf, cleanup := tests.SetupTest() 50 | defer cleanup() 51 | 52 | app := &cli.App{Commands: []*cli.Command{PickCommand}} 53 | args := []string{"test-app", "pick", "--number", "2", "a", "b", "c"} 54 | 55 | if err := app.Run(args); err != nil { 56 | t.Errorf("expected no error, got %v", err) 57 | } 58 | 59 | output := strings.TrimSpace(buf.String()) 60 | items := strings.Split(output, common.DEFAULT_DELIMITER) 61 | if len(items) != 2 { 62 | t.Errorf("expected 2 items, got %d", len(items)) 63 | } 64 | } 65 | 66 | func TestPickCommand_Delimiter(t *testing.T) { 67 | buf, cleanup := tests.SetupTest() 68 | defer cleanup() 69 | 70 | app := &cli.App{Commands: []*cli.Command{PickCommand}} 71 | args := []string{"test-app", "pick", "--delimiter", ",", "--number", "2", "a", "b", "c"} 72 | 73 | if err := app.Run(args); err != nil { 74 | t.Errorf("expected no error, got %v", err) 75 | } 76 | 77 | output := strings.TrimSpace(buf.String()) 78 | items := strings.Split(output, ",") 79 | if len(items) != 2 { 80 | t.Errorf("expected 2 items, got %d", len(items)) 81 | } 82 | 83 | for _, item := range items { 84 | if !slices.Contains([]string{"a", "b", "c"}, item) { 85 | t.Errorf("output item '%s' is not one of the input items", item) 86 | } 87 | } 88 | } 89 | 90 | func TestPickCommand_EmptyInput(t *testing.T) { 91 | buf, cleanup := tests.SetupTest() 92 | defer cleanup() 93 | 94 | app := &cli.App{Commands: []*cli.Command{PickCommand}} 95 | args := []string{"test-app", "pick"} 96 | 97 | if err := app.Run(args); err != nil { 98 | t.Errorf("expected no error, got %v", err) 99 | } 100 | 101 | output := strings.TrimSpace(buf.String()) 102 | if output != "" { 103 | t.Errorf("expected empty output, got '%s'", output) 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/common/helpers/random.go: -------------------------------------------------------------------------------- 1 | package helpers 2 | 3 | import ( 4 | "crypto/rand" 5 | "fmt" 6 | "math" 7 | 8 | "github.com/AmrSaber/random/v3/src/common" 9 | ) 10 | 11 | // returns a random floating point number created with 12 | // cryptographically secure pseudo random generator (crypto). 13 | func GetSecureFloat() float64 { 14 | // Generate 7 random bytes 15 | bytes := make([]byte, 7) 16 | rand.Read(bytes) 17 | 18 | // Shift 5 bits from first byte by 5 to the right 19 | randomValue := float64(bytes[0]%(1<<5)) / float64(1<<5) 20 | 21 | // For each of the following 6 bytes, add its value and shift it 8 bits to the right 22 | for _, byte := range bytes[1:] { 23 | randomValue = (randomValue + float64(byte)) / float64(1<<8) 24 | } 25 | 26 | return randomValue 27 | } 28 | 29 | // returns a random integer in the given range, 30 | // both start and end are inclusive. 31 | func GetRandomIntInRange(min int, max int) int { 32 | if min > max { 33 | common.Fail("min must be less than or equal to max") 34 | } 35 | 36 | return int(math.Floor(GetSecureFloat()*float64(max-min+1))) + min 37 | } 38 | 39 | // returns a random element from the given slice. 40 | func GetRandomElement[T any](arr []T) T { 41 | if len(arr) == 0 { 42 | common.Fail("array is empty") 43 | } 44 | 45 | randomIndex := GetRandomIntInRange(0, len(arr)-1) 46 | return arr[randomIndex] 47 | } 48 | 49 | // returns boolean values for the given type. 50 | func getBooleanTypeValues(boolType string) []any { 51 | switch boolType { 52 | case common.BOOLEAN_TYPE_NUMERIC: 53 | return []any{1, 0} 54 | case common.BOOLEAN_TYPE_YES_NO: 55 | return []any{"yes", "no"} 56 | case common.BOOLEAN_TYPE_TRUE_FALSE: 57 | return []any{true, false} 58 | default: 59 | common.Fail(fmt.Sprintf("unknown boolean type [%s]", boolType)) 60 | return nil 61 | } 62 | } 63 | 64 | // returns a random boolean of the specified type. 65 | func GetRandomBoolean(boolType string) any { 66 | values := getBooleanTypeValues(boolType) 67 | return GetRandomElement(values) 68 | } 69 | 70 | // returns a shuffled array with the elements [start .. end] 71 | // This uses a variation of fisher-yates algorithm 72 | func GetShuffledArray(start, end int) []int { 73 | shuffled := make([]int, 0, end-start+1) 74 | 75 | for value := start; value <= end; value++ { 76 | randomIndex := GetRandomIntInRange(0, len(shuffled)) 77 | 78 | if randomIndex == len(shuffled) { 79 | shuffled = append(shuffled, value) 80 | } else { 81 | shuffled = append(shuffled, shuffled[randomIndex]) 82 | shuffled[randomIndex] = value 83 | } 84 | } 85 | 86 | return shuffled 87 | } 88 | 89 | // returns a shuffled copy of the input slice 90 | func Shuffle[T any](arr []T) []T { 91 | shuffledIndexes := GetShuffledArray(0, len(arr)-1) 92 | result := make([]T, len(arr)) 93 | 94 | for i, idx := range shuffledIndexes { 95 | result[i] = arr[idx] 96 | } 97 | 98 | return result 99 | } 100 | 101 | // returns an array of valid characters for the given type 102 | func getValidCharacters(strType string) []rune { 103 | switch strType { 104 | case common.STRING_TYPE_ASCII: 105 | return append(common.ASCII_LETTERS, common.NUMBERS...) 106 | case common.STRING_TYPE_NUMBERS: 107 | return common.NUMBERS 108 | case common.STRING_TYPE_LETTERS: 109 | return common.ASCII_LETTERS 110 | case common.STRING_TYPE_EXTENDED: 111 | return append(append(common.ASCII_LETTERS, common.NUMBERS...), []rune("+-_$#/@!")...) 112 | case common.STRING_TYPE_HEX: 113 | return common.HEX_DIGITS 114 | case common.STRING_TYPE_BASE_64: 115 | return append(append(common.ASCII_LETTERS, common.NUMBERS...), []rune("+/")...) 116 | default: 117 | common.Fail(fmt.Sprintf("unknown type [%s]", strType)) 118 | return nil 119 | } 120 | } 121 | 122 | // returns a random string of the given type and length 123 | func GetRandomString(strType string, length int) string { 124 | valid := getValidCharacters(strType) 125 | randomLetters := make([]rune, 0, length) 126 | 127 | for i := 0; i < length; i++ { 128 | randomLetters = append(randomLetters, GetRandomElement(valid)) 129 | } 130 | 131 | // Base64 string must have length that is a multiple of 4 132 | if strType == common.STRING_TYPE_BASE_64 { 133 | for len(randomLetters)%4 != 0 { 134 | randomLetters = append(randomLetters, '=') 135 | } 136 | } 137 | 138 | return string(randomLetters) 139 | } 140 | --------------------------------------------------------------------------------