Repository: peak/s5cmd (go)

Issue:
disable wildcard and use "*" as literal?
The S3 objects I'm going to download has "\*" in object key. When I use `cp` to download, `s5cmd` expands it and download multiple objects more than the particular object. Is there a way to escape wildcard "\*" or disable wildcard? Thanks.

Pull request:
command/cp&rm: add `--raw` flag
- command/cp&rm:  new flag `--raw` which disables the wildcard operations.
- also changed the `rb` command's helper text to correct form, based on [this commit](https://github.com/peak/s5cmd/commit/68f797b0de6b26f311076012928a9aa3aaad2713).



resolves #235 

Patch:
diff --git a/command/cat.go b/command/cat.go
index db83ab6..b983a1e 100644
--- a/command/cat.go
+++ b/command/cat.go
@@ -112,7 +112,7 @@ func validateCatCommand(c *cli.Context) error {
 		return fmt.Errorf("remote source must be an object")
 	}
 
-	if src.HasGlob() {
+	if src.IsWildcard() {
 		return fmt.Errorf("remote source %q can not contain glob characters", src)
 	}
 	return nil
diff --git a/command/cp.go b/command/cp.go
index 33cd12e..81cab7e 100644
--- a/command/cp.go
+++ b/command/cp.go
@@ -163,6 +163,10 @@ var copyCommandFlags = []cli.Flag{
 		Name:  "destination-region",
 		Usage: "set the region of destination bucket: the region of the destination bucket will be automatically discovered if --destination-region is not specified",
 	},
+	&cli.BoolFlag{
+		Name:  "raw",
+		Usage: "disable the wildcard operations, useful with filenames that contains glob characters.",
+	},
 }
 
 var copyCommand = &cli.Command{
@@ -200,6 +204,7 @@ var copyCommand = &cli.Command{
 			encryptionKeyID:      c.String("sse-kms-key-id"),
 			acl:                  c.String("acl"),
 			forceGlacierTransfer: c.Bool("force-glacier-transfer"),
+			raw:                  c.Bool("raw"),
 			cacheControl:         c.String("cache-control"),
 			expires:              c.String("expires"),
 			// region settings
@@ -231,10 +236,10 @@ type Copy struct {
 	encryptionKeyID      string
 	acl                  string
 	forceGlacierTransfer bool
+	raw                  bool
 	cacheControl         string
 	expires              string
 
-
 	// region settings
 	srcRegion string
 	dstRegion string
@@ -253,13 +258,13 @@ increase the open file limit or try to decrease the number of workers with
 
 // Run starts copying given source objects to destination.
 func (c Copy) Run(ctx context.Context) error {
-	srcurl, err := url.New(c.src)
+	srcurl, err := url.New(c.src, url.WithRaw(c.raw))
 	if err != nil {
 		printError(c.fullCommand, c.op, err)
 		return err
 	}
 
-	dsturl, err := url.New(c.dst)
+	dsturl, err := url.New(c.dst, url.WithRaw(c.raw))
 	if err != nil {
 		printError(c.fullCommand, c.op, err)
 		return err
@@ -269,6 +274,7 @@ func (c Copy) Run(ctx context.Context) error {
 	if c.srcRegion != "" {
 		c.storageOpts.SetRegion(c.srcRegion)
 	}
+
 	client, err := storage.NewClient(ctx, srcurl, c.storageOpts)
 	if err != nil {
 		printError(c.fullCommand, c.op, err)
@@ -276,6 +282,7 @@ func (c Copy) Run(ctx context.Context) error {
 	}
 
 	objch, err := expandSource(ctx, client, c.followSymlinks, srcurl)
+
 	if err != nil {
 		printError(c.fullCommand, c.op, err)
 		return err
@@ -302,7 +309,7 @@ func (c Copy) Run(ctx context.Context) error {
 		}
 	}()
 
-	isBatch := srcurl.HasGlob()
+	isBatch := srcurl.IsWildcard()
 	if !isBatch && !srcurl.IsRemote() {
 		obj, _ := client.Stat(ctx, srcurl)
 		isBatch = obj != nil && obj.Type.IsDir()
@@ -379,7 +386,6 @@ func (c Copy) prepareDownloadTask(
 		if err != nil {
 			return err
 		}
-
 		err = c.doDownload(ctx, srcurl, dsturl)
 		if err != nil {
 			return &errorpkg.Error{
@@ -739,18 +745,18 @@ func validateCopyCommand(c *cli.Context) error {
 	src := c.Args().Get(0)
 	dst := c.Args().Get(1)
 
-	srcurl, err := url.New(src)
+	srcurl, err := url.New(src, url.WithRaw(c.Bool("raw")))
 	if err != nil {
 		return err
 	}
 
-	dsturl, err := url.New(dst)
+	dsturl, err := url.New(dst, url.WithRaw(c.Bool("raw")))
 	if err != nil {
 		return err
 	}
 
 	// wildcard destination doesn't mean anything
-	if dsturl.HasGlob() {
+	if dsturl.IsWildcard() {
 		return fmt.Errorf("target %q can not contain glob characters", dst)
 	}
 
@@ -761,7 +767,7 @@ func validateCopyCommand(c *cli.Context) error {
 
 	// 'cp dir/* s3://bucket/prefix': expect a trailing slash to avoid any
 	// surprises.
-	if srcurl.HasGlob() && dsturl.IsRemote() && !dsturl.IsPrefix() && !dsturl.IsBucket() {
+	if srcurl.IsWildcard() && dsturl.IsRemote() && !dsturl.IsPrefix() && !dsturl.IsBucket() {
 		return fmt.Errorf("target %q must be a bucket or a prefix", dsturl)
 	}
 
@@ -787,7 +793,7 @@ func validateCopy(srcurl, dsturl *url.URL) error {
 func validateUpload(ctx context.Context, srcurl, dsturl *url.URL, storageOpts storage.Options) error {
 	srcclient := storage.NewLocalClient(storageOpts)
 
-	if srcurl.HasGlob() {
+	if srcurl.IsWildcard() {
 		return nil
 	}
 
diff --git a/command/expand.go b/command/expand.go
index d3467f6..2954f59 100644
--- a/command/expand.go
+++ b/command/expand.go
@@ -21,7 +21,7 @@ func expandSource(
 	// if the source is local, we send a Stat call to know if  we have
 	// directory or file to walk. For remote storage, we don't want to send
 	// Stat since it doesn't have any folder semantics.
-	if !srcurl.HasGlob() && !srcurl.IsRemote() {
+	if !srcurl.IsWildcard() && !srcurl.IsRemote() {
 		obj, err := client.Stat(ctx, srcurl)
 		if err != nil {
 			return nil, err
@@ -30,7 +30,7 @@ func expandSource(
 	}
 
 	// call storage.List for only walking operations.
-	if srcurl.HasGlob() || isDir {
+	if srcurl.IsWildcard() || isDir {
 		return client.List(ctx, srcurl, followSymlinks), nil
 	}
 
diff --git a/command/rb.go b/command/rb.go
index e222b1a..558c055 100644
--- a/command/rb.go
+++ b/command/rb.go
@@ -15,14 +15,14 @@ var removeBucketHelpTemplate = `Name:
 	{{.HelpName}} - {{.Usage}}
 
 Usage:
-	{{.HelpName}} bucketname
+	{{.HelpName}} s3://bucketname
 
 Options:
 	{{range .VisibleFlags}}{{.}}
 	{{end}}
 Examples:
 	1. Deletes S3 bucket with given name
-		 > s5cmd {{.HelpName}} bucketname
+		 > s5cmd {{.HelpName}} s3://bucketname
 `
 
 var removeBucketCommand = &cli.Command{
diff --git a/command/rm.go b/command/rm.go
index 148ee0e..98f56ea 100644
--- a/command/rm.go
+++ b/command/rm.go
@@ -37,10 +37,18 @@ Examples:
 		 > s5cmd {{.HelpName}} s3://bucketname/prefix/* s3://bucketname/object1.gz
 `
 
+var deleteCommandFlags = []cli.Flag{
+	&cli.BoolFlag{
+		Name:  "raw",
+		Usage: "disable the wildcard operations, useful with filenames that contains glob characters.",
+	},
+}
+
 var deleteCommand = &cli.Command{
 	Name:               "rm",
 	HelpName:           "rm",
 	Usage:              "remove objects",
+	Flags:              deleteCommandFlags,
 	CustomHelpTemplate: deleteHelpTemplate,
 	Before: func(c *cli.Context) error {
 		err := validateRMCommand(c)
@@ -55,6 +63,7 @@ var deleteCommand = &cli.Command{
 			src:         c.Args().Slice(),
 			op:          c.Command.Name,
 			fullCommand: givenCommand(c),
+			raw:         c.Bool("raw"),
 			storageOpts: NewStorageOpts(c),
 		}.Run(c.Context)
 	},
@@ -65,6 +74,7 @@ type Delete struct {
 	src         []string
 	op          string
 	fullCommand string
+	raw         bool
 
 	// storage options
 	storageOpts storage.Options
@@ -72,7 +82,7 @@ type Delete struct {
 
 // Run remove given sources.
 func (d Delete) Run(ctx context.Context) error {
-	srcurls, err := newURLs(d.src...)
+	srcurls, err := newURLs(d.raw, d.src...)
 	if err != nil {
 		printError(d.fullCommand, d.op, err)
 		return err
@@ -85,14 +95,14 @@ func (d Delete) Run(ctx context.Context) error {
 		return err
 	}
 
-	objChan := expandSources(ctx, client, false, srcurls...)
+	objch := expandSources(ctx, client, false, srcurls...)
 
 	// do object->url transformation
 	urlch := make(chan *url.URL)
 	go func() {
 		defer close(urlch)
 
-		for object := range objChan {
+		for object := range objch {
 			if object.Type.IsDir() || errorpkg.IsCancelation(object.Err) {
 				continue
 			}
@@ -130,10 +140,10 @@ func (d Delete) Run(ctx context.Context) error {
 }
 
 // newSources creates object URL list from given sources.
-func newURLs(sources ...string) ([]*url.URL, error) {
+func newURLs(urlMode bool, sources ...string) ([]*url.URL, error) {
 	var urls []*url.URL
 	for _, src := range sources {
-		srcurl, err := url.New(src)
+		srcurl, err := url.New(src, url.WithRaw(urlMode))
 		if err != nil {
 			return nil, err
 		}
@@ -147,7 +157,7 @@ func validateRMCommand(c *cli.Context) error {
 		return fmt.Errorf("expected at least 1 object to remove")
 	}
 
-	srcurls, err := newURLs(c.Args().Slice()...)
+	srcurls, err := newURLs(c.Bool("raw"), c.Args().Slice()...)
 	if err != nil {
 		return err
 	}
diff --git a/command/select.go b/command/select.go
index 42a4f8d..fe50933 100644
--- a/command/select.go
+++ b/command/select.go
@@ -205,7 +205,7 @@ func validateSelectCommand(c *cli.Context) error {
 		return fmt.Errorf("source must be remote")
 	}
 
-      if !strings.EqualFold(c.String("format"), "JSON") {
+	if !strings.EqualFold(c.String("format"), "JSON") {
 		return fmt.Errorf("only json supported")
 	}
 
diff --git a/storage/fs.go b/storage/fs.go
index 86d8327..37ce48b 100644
--- a/storage/fs.go
+++ b/storage/fs.go
@@ -39,7 +39,7 @@ func (f *Filesystem) Stat(ctx context.Context, url *url.URL) (*Object, error) {
 
 // List returns the objects and directories reside in given src.
 func (f *Filesystem) List(ctx context.Context, src *url.URL, followSymlinks bool) <-chan *Object {
-	if src.HasGlob() {
+	if src.IsWildcard() {
 		return f.expandGlob(ctx, src, followSymlinks)
 	}
 
diff --git a/storage/url/url.go b/storage/url/url.go
index 01c1ac6..df56d5e 100644
--- a/storage/url/url.go
+++ b/storage/url/url.go
@@ -45,10 +45,19 @@ type URL struct {
 	relativePath string
 	filter       string
 	filterRegex  *regexp.Regexp
+	raw          bool
+}
+
+type Option func(u *URL)
+
+func WithRaw(mode bool) Option {
+	return func(u *URL) {
+		u.raw = mode
+	}
 }
 
 // New creates a new URL from given path string.
-func New(s string) (*URL, error) {
+func New(s string, opts ...Option) (*URL, error) {
 	split := strings.Split(s, "://")
 
 	if len(split) == 1 {
@@ -57,6 +66,11 @@ func New(s string) (*URL, error) {
 			Scheme: "",
 			Path:   s,
 		}
+
+		for _, opt := range opts {
+			opt(url)
+		}
+
 		if err := url.setPrefixAndFilter(); err != nil {
 			return nil, err
 		}
@@ -100,6 +114,10 @@ func New(s string) (*URL, error) {
 		Path:   key,
 	}
 
+	for _, opt := range opts {
+		opt(url)
+	}
+
 	if err := url.setPrefixAndFilter(); err != nil {
 		return nil, err
 	}
@@ -214,6 +232,10 @@ func (u *URL) remoteURL() string {
 //		delimiter: "/"
 //
 func (u *URL) setPrefixAndFilter() error {
+	if u.raw {
+		return nil
+	}
+
 	loc := strings.IndexAny(u.Path, globCharacters)
 	wildOperation := loc > -1
 	if !wildOperation {
@@ -289,9 +311,9 @@ func (u *URL) MarshalJSON() ([]byte, error) {
 	return json.Marshal(u.String())
 }
 
-// HasGlob reports whether if a string contains any wildcard chars.
-func (u *URL) HasGlob() bool {
-	return hasGlobCharacter(u.Path)
+// IsWildcard reports whether if a string contains any wildcard chars.
+func (u *URL) IsWildcard() bool {
+	return !u.raw && hasGlobCharacter(u.Path)
 }
 
 // parseBatch parses keys for wildcard operations.


Test patch:
diff --git a/command/expand_test.go b/command/expand_test.go
index ec4f469..059da8e 100644
--- a/command/expand_test.go
+++ b/command/expand_test.go
@@ -132,9 +132,7 @@ func TestExpandSources(t *testing.T) {
 	for _, tc := range tests {
 		tc := tc
 		t.Run(tc.name, func(t *testing.T) {
-			// t.Parallel()
-
-			srcurls, err := newURLs(keys(tc.src)...)
+			srcurls, err := newURLs(false, keys(tc.src)...)
 			if err != nil {
 				t.Errorf("unexpected error: %v", err)
 				return
diff --git a/e2e/cp_test.go b/e2e/cp_test.go
index 4aca7ed..375aa1f 100644
--- a/e2e/cp_test.go
+++ b/e2e/cp_test.go
@@ -2926,3 +2926,423 @@ func TestCopyS3ToDirDryRun(t *testing.T) {
 		assert.Assert(t, ensureS3Object(s3client, bucket, f, "content"))
 	}
 }
+
+func TestCopyLocalObjectstoS3WithRawFlag(t *testing.T) {
+	if runtime.GOOS == "windows" {
+		t.Skip()
+	}
+
+	t.Parallel()
+	const (
+		bucket      = "bucket"
+		fileContent = "this is a file content"
+	)
+
+	testcases := []struct {
+		name             string
+		src              []fs.PathOp
+		dst              string
+		wantedFile       string
+		expectedFiles    []string
+		nonExpectedFiles []string
+		rawFlag          string
+	}{
+		{
+			name: "cp --raw file*.txt s3://bucket/",
+			src: []fs.PathOp{
+				fs.WithFile("file*.txt", "content"),
+				fs.WithFile("file*1.txt", "content"),
+				fs.WithFile("file*file.txt", "content"),
+				fs.WithFile("file*2.txt", "content"),
+			},
+			wantedFile:       "file*.txt",
+			dst:              "s3://bucket/",
+			expectedFiles:    []string{"file*.txt"},
+			nonExpectedFiles: []string{"file*1.txt", "file*file.txt", "file*2.txt"},
+			rawFlag:          "--raw",
+		},
+		{
+			name: "cp  file*.txt s3://bucket/",
+			src: []fs.PathOp{
+				fs.WithFile("file*.txt", "content"),
+				fs.WithFile("file*1.txt", "content"),
+				fs.WithFile("file*file.txt", "content"),
+				fs.WithFile("file*2.txt", "content"),
+			},
+			wantedFile:       "file*.txt",
+			dst:              "s3://bucket/",
+			expectedFiles:    []string{"file*.txt", "file*1.txt", "file*file.txt", "file*2.txt"},
+			nonExpectedFiles: []string{},
+			rawFlag:          "",
+		},
+		{
+			name: "cp  a*/file*.txt s3://bucket/",
+			src: []fs.PathOp{
+				fs.WithDir(
+					"a*",
+					fs.WithFile("file*.txt", "content"),
+					fs.WithFile("file*1.txt", "content"),
+				),
+				fs.WithDir(
+					"a*b",
+					fs.WithFile("file*2.txt", "content"),
+					fs.WithFile("file*3.txt", "content"),
+				),
+
+				fs.WithFile("file4.txt", "content"),
+			},
+			wantedFile:       "a*/file*.txt",
+			dst:              "s3://bucket/",
+			expectedFiles:    []string{"file*.txt"}, // when full path entered, the base part is uploaded.
+			nonExpectedFiles: []string{"a*/file*.txt", "a*/file*1.txt", "a*b/file*2.txt", "a*/file*3.txt", "file*4.txt", "file*1.txt", "file*2.txt", "file*3.txt"},
+			rawFlag:          "--raw",
+		},
+	}
+
+	for _, tc := range testcases {
+		tc := tc
+		t.Run(tc.name, func(t *testing.T) {
+			t.Parallel()
+
+			s3client, s5cmd, cleanup := setup(t)
+			defer cleanup()
+
+			createBucket(t, s3client, bucket)
+
+			workdir := fs.NewDir(t, "copy-raw-test", tc.src...)
+			defer workdir.Remove()
+
+			srcpath := filepath.ToSlash(workdir.Join(tc.wantedFile))
+
+			cmd := s5cmd("cp", srcpath, tc.dst)
+			if tc.rawFlag != "" {
+				cmd = s5cmd("cp", tc.rawFlag, srcpath, tc.dst)
+			}
+
+			result := icmd.RunCmd(cmd)
+			result.Assert(t, icmd.Success)
+
+			for _, obj := range tc.expectedFiles {
+				err := ensureS3Object(s3client, bucket, obj, "content")
+				if err != nil {
+					t.Fatalf("%s is not exist in s3\n", obj)
+				}
+			}
+
+			for _, obj := range tc.nonExpectedFiles {
+				err := ensureS3Object(s3client, bucket, obj, "content")
+				assertError(t, err, errS3NoSuchKey)
+			}
+
+			// assert filesystem
+			expected := fs.Expected(t, tc.src...)
+			assert.Assert(t, fs.Equal(workdir.Path(), expected))
+		})
+	}
+}
+
+// When folder is uploaded with --raw flag, it only uploads file with given name.
+func TestCopyDirToS3WithRawFlag(t *testing.T) {
+	if runtime.GOOS == "windows" {
+		t.Skip()
+	}
+
+	t.Parallel()
+
+	bucket := s3BucketFromTestName(t)
+
+	s3client, s5cmd, cleanup := setup(t)
+	defer cleanup()
+
+	createBucket(t, s3client, bucket)
+
+	folderLayout := []fs.PathOp{
+		fs.WithDir(
+			"a*",
+			fs.WithFile("file*.txt", "content"),
+			fs.WithFile("file*1.txt", "content"),
+		),
+		fs.WithDir(
+			"a*b",
+			fs.WithFile("file*2.txt", "content"),
+			fs.WithFile("file*3.txt", "content"),
+		),
+
+		fs.WithFile("file*4.txt", "content"),
+	}
+
+	workdir := fs.NewDir(t, t.Name(), folderLayout...)
+	defer workdir.Remove()
+
+	srcpath := filepath.ToSlash(workdir.Join("a*"))
+	dstpath := fmt.Sprintf("s3://%v", bucket)
+
+	cmd := s5cmd("cp", "--raw", srcpath, dstpath)
+	result := icmd.RunCmd(cmd)
+
+	result.Assert(t, icmd.Success)
+
+	assertLines(t, result.Stdout(), map[int]compareFunc{
+		0: equals("cp %v/file*.txt %v/a*/file*.txt", srcpath, dstpath),
+		1: equals("cp %v/file*1.txt %v/a*/file*1.txt", srcpath, dstpath),
+	}, sortInput(true))
+
+	expectedObjs := []string{"a*/file*.txt", "a*/file*1.txt"}
+	for _, obj := range expectedObjs {
+		err := ensureS3Object(s3client, bucket, obj, "content")
+		if err != nil {
+			t.Fatalf("Object %s is not in S3\n", obj)
+		}
+	}
+
+	nonExpectedObjs := []string{"a*b/file*2.txt", "a*b/file*3.txt", "file*.txt", "file*1.txt", "file*2.txt", "file*3.txt", "file*4.txt"}
+	for _, obj := range nonExpectedObjs {
+		err := ensureS3Object(s3client, bucket, obj, "content")
+		assertError(t, err, errS3NoSuchKey)
+	}
+
+	// assert local filesystem
+	expected := fs.Expected(t, folderLayout...)
+	assert.Assert(t, fs.Equal(workdir.Path(), expected))
+}
+
+func TestCopyS3ObjectstoLocalWithRawFlag(t *testing.T) {
+	if runtime.GOOS == "windows" {
+		t.Skip()
+	}
+
+	t.Parallel()
+	const (
+		bucket      = "bucket"
+		fileContent = "this is a file content"
+	)
+
+	testcases := []struct {
+		name           string
+		src            []string
+		wantedFile     string
+		expectedOutput string
+		expectedFiles  []fs.PathOp
+		rawFlag        string
+	}{
+		{
+			name:           "cp --raw file*.txt s3://bucket/",
+			src:            []string{"file*.txt", "file*1.txt", "file*2.txt"},
+			wantedFile:     "file*.txt",
+			expectedOutput: "cp s3://bucket/file*.txt file*txt",
+			rawFlag:        "--raw",
+			expectedFiles: []fs.PathOp{
+				fs.WithFile("file*.txt", fileContent),
+			},
+		},
+		{
+			name:       "cp  file*.txt s3://bucket/",
+			src:        []string{"file*.txt", "file*1.txt", "file*2.txt"},
+			wantedFile: "file*.txt",
+			rawFlag:    "",
+			expectedFiles: []fs.PathOp{
+				fs.WithFile("file*.txt", fileContent),
+				fs.WithFile("file*1.txt", fileContent),
+				fs.WithFile("file*2.txt", fileContent),
+			},
+		},
+		{
+			name:       "cp  a*/file.txt s3://bucket/",
+			src:        []string{"a*/file*.txt", "a*b/file1.txt", "a*c/file2.txt"},
+			wantedFile: "a*/file*.txt",
+			rawFlag:    "--raw",
+			expectedFiles: []fs.PathOp{
+				fs.WithFile("file*.txt", fileContent),
+			},
+		},
+		{
+			name:       "cp  a*/file.txt s3://bucket/",
+			src:        []string{"a*/file.txt", "a*/file1.txt", "a*/file2.txt"},
+			wantedFile: "a*/file.txt",
+			rawFlag:    "",
+			expectedFiles: []fs.PathOp{
+				fs.WithDir(
+					"a*",
+					fs.WithFile("file.txt", fileContent),
+				),
+			},
+		},
+	}
+
+	for _, tc := range testcases {
+		tc := tc
+		t.Run(tc.name, func(t *testing.T) {
+			t.Parallel()
+
+			s3client, s5cmd, cleanup := setup(t)
+			defer cleanup()
+
+			createBucket(t, s3client, bucket)
+
+			for _, filename := range tc.src {
+				putFile(t, s3client, bucket, filename, fileContent)
+
+			}
+
+			cmd := s5cmd("cp", "s3://"+bucket+"/"+tc.wantedFile, ".")
+			if tc.rawFlag != "" {
+				cmd = s5cmd("cp", "--raw", "s3://"+bucket+"/"+tc.wantedFile, ".")
+			}
+
+			result := icmd.RunCmd(cmd)
+
+			result.Assert(t, icmd.Success)
+
+			// assert local file system
+			expected := fs.Expected(t, tc.expectedFiles...)
+			assert.Assert(t, fs.Equal(cmd.Dir, expected))
+
+			// assert s3 object
+			for _, filename := range tc.src {
+				assert.Assert(t, ensureS3Object(s3client, bucket, filename, fileContent))
+			}
+		})
+	}
+}
+
+func TestCopyMultipleS3ObjectsToS3WithRawMode(t *testing.T) {
+	t.Parallel()
+
+	const bucket = "bucket"
+	const destBucket = "destbucket"
+
+	s3client, s5cmd, cleanup := setup(t)
+	defer cleanup()
+
+	createBucket(t, s3client, bucket)
+	createBucket(t, s3client, destBucket)
+
+	filesToContent := map[string]string{
+		"file*.txt":      "this is a test file 1",
+		"file*1.txt":     "this is a test file 2",
+		"file*.py":       "this is a test python file",
+		"file*/file.txt": "this is a test file with prefix",
+	}
+
+	for filename, content := range filesToContent {
+		putFile(t, s3client, bucket, filename, content)
+	}
+
+	src := fmt.Sprintf("s3://%v/file*.txt", bucket)
+	dst := fmt.Sprintf("s3://%v", destBucket)
+
+	cmd := s5cmd("cp", "--raw", src, dst)
+	result := icmd.RunCmd(cmd)
+
+	result.Assert(t, icmd.Success)
+
+	assertLines(t, result.Stdout(), map[int]compareFunc{
+		0: equals("cp %v %v/file*.txt", src, dst),
+	})
+
+	// assert s3 source objects
+	for filename, content := range filesToContent {
+		assert.Assert(t, ensureS3Object(s3client, bucket, filename, content))
+	}
+
+	expectedFiles := map[string]string{
+		"file*.txt": "this is a test file 1",
+	}
+
+	for filename, content := range expectedFiles {
+		assert.Assert(t, ensureS3Object(s3client, destBucket, filename, content))
+	}
+}
+
+// cp --raw s3://bucket/file* s3://destbucket
+func TestCopyMultipleS3ObjectsWithPrefixToS3WithRawMode(t *testing.T) {
+	t.Parallel()
+
+	const bucket = "bucket"
+	const destBucket = "destbucket"
+
+	s3client, s5cmd, cleanup := setup(t)
+	defer cleanup()
+
+	createBucket(t, s3client, bucket)
+	createBucket(t, s3client, destBucket)
+
+	filesToContent := map[string]string{
+		"file*/file.txt":   "this is a test file 1 in file*",
+		"file*/file1.txt":  "this is a test file 2 in file*",
+		"file*a/file.txt":  "this is a test file 1 in file*b",
+		"file*a/file1.txt": "this is a test file 2 in file*b",
+	}
+
+	for filename, content := range filesToContent {
+		putFile(t, s3client, bucket, filename, content)
+	}
+
+	src := fmt.Sprintf("s3://%v/file*", bucket)
+	dst := fmt.Sprintf("s3://%v", destBucket)
+
+	cmd := s5cmd("cp", "--raw", src, dst)
+	result := icmd.RunCmd(cmd)
+
+	result.Assert(t, icmd.Expected{ExitCode: 1})
+
+	expected := fmt.Sprintf(`ERROR "cp %v %v/file*": NoSuchKey:`, src, dst)
+
+	assertLines(t, result.Stderr()[:len(expected)], map[int]compareFunc{
+		0: equals(expected),
+	})
+}
+
+// cp --raw s3://bucket/file* s3://destbucket
+func TestCopyRawModeAllowDestinationWithoutPrefix(t *testing.T) {
+	t.Parallel()
+
+	const bucket = "bucket"
+
+	s3client, s5cmd, cleanup := setup(t)
+	defer cleanup()
+
+	createBucket(t, s3client, bucket)
+
+	filesToContent := map[string]string{
+		"test*/file.txt": "this is a test file 1 in file*",
+	}
+
+	for filename, content := range filesToContent {
+		putFile(t, s3client, bucket, filename, content)
+	}
+
+	folderLayout := []fs.PathOp{
+		fs.WithFile("testfile.txt", "this is a test file 1"),
+		fs.WithFile("readme.md", "this is a readme file"),
+		fs.WithDir(
+			"a",
+			fs.WithFile("another_test_file.txt", "yet another txt file. yatf."),
+		),
+		fs.WithDir(
+			"b",
+			fs.WithFile("filename-with-hypen.gz", "file has hypen in its name"),
+		),
+	}
+
+	workdir := fs.NewDir(t, "somedir", folderLayout...)
+	defer workdir.Remove()
+
+	src := fmt.Sprintf("%v/testfile.txt", workdir.Path())
+	src = filepath.ToSlash(src)
+	dst := fmt.Sprintf("s3://%s/test*/", bucket)
+
+	cmd := s5cmd("cp", "--raw", src, dst)
+	result := icmd.RunCmd(cmd)
+
+	result.Assert(t, icmd.Success)
+
+	assertLines(t, result.Stdout(), map[int]compareFunc{
+		0: equals("cp %v %vtestfile.txt", src, dst),
+	})
+
+	err := ensureS3Object(s3client, bucket, "test*/testfile.txt", "this is a test file 1")
+	if err != nil {
+		t.Errorf("testfile*.txt not exist in S3 bucket %v\n", dst)
+	}
+}
diff --git a/e2e/rm_test.go b/e2e/rm_test.go
index 3075e6c..91fbf95 100644
--- a/e2e/rm_test.go
+++ b/e2e/rm_test.go
@@ -660,3 +660,142 @@ func TestRemoveMultipleS3ObjectsDryRun(t *testing.T) {
 		assert.Assert(t, ensureS3Object(s3client, bucket, filename, content))
 	}
 }
+
+func TestRemoveS3ObjectRawFlag(t *testing.T) {
+	t.Parallel()
+
+	bucket := s3BucketFromTestName(t)
+
+	s3client, s5cmd, cleanup := setup(t)
+	defer cleanup()
+
+	createBucket(t, s3client, bucket)
+
+	filesToContent := map[string]string{
+		"file*.txt":  "this is a test file 1",
+		"file*1.txt": "this is a test file 2",
+		"file*.py":   "test file 1 python version",
+		"file*.c":    "test file 1 c version.",
+	}
+
+	nonDeletedFiles := map[string]string{
+		"file*1.txt": "this is a test file 2",
+		"file*.py":   "test file 1 python version",
+		"file*.c":    "test file 1 c version.",
+	}
+
+	deletedFiles := map[string]string{
+		"file*.txt": "this is a test file 1",
+	}
+
+	for filename, content := range filesToContent {
+		putFile(t, s3client, bucket, filename, content)
+	}
+
+	cmd := s5cmd("rm", "-raw", "s3://"+bucket+"/file*.txt")
+	result := icmd.RunCmd(cmd)
+
+	result.Assert(t, icmd.Success)
+
+	assertLines(t, result.Stderr(), map[int]compareFunc{})
+
+	assertLines(t, result.Stdout(), map[int]compareFunc{
+		0: equals(`rm s3://%v/file*.txt`, bucket),
+	})
+
+	// ensure files which is not supposed to be deleted.
+	for filename, content := range nonDeletedFiles {
+		assert.Assert(t, ensureS3Object(s3client, bucket, filename, content))
+	}
+
+	// add no such key
+	for filename, content := range deletedFiles {
+		err := ensureS3Object(s3client, bucket, filename, content)
+		assertError(t, err, errS3NoSuchKey)
+	}
+}
+
+func TestRemoveS3ObjectsPrefixRawFlag(t *testing.T) {
+	t.Parallel()
+
+	bucket := s3BucketFromTestName(t)
+
+	s3client, s5cmd, cleanup := setup(t)
+	defer cleanup()
+
+	createBucket(t, s3client, bucket)
+
+	filesToContent := map[string]string{
+		"abc*/file.txt":  "this is a test file 1",
+		"abc*/file1.txt": "this is a test file 2",
+		"abc*/file.py":   "test file 1 python version",
+		"abc*/file.c":    "test file 1 c version.",
+		"abcd/file.txt":  "this is a test file with different prefix",
+	}
+
+	nonDeletedFiles := map[string]string{
+		"abc*/file1.txt": "this is a test file 2",
+		"abc*/file.py":   "test file 1 python version",
+		"abc*/file.c":    "test file 1 c version.",
+		"abcd/file.txt":  "this is a test file with different prefix",
+	}
+
+	for filename, content := range filesToContent {
+		putFile(t, s3client, bucket, filename, content)
+	}
+
+	cmd := s5cmd("rm", "-raw", "s3://"+bucket+"/abc*/file.txt")
+	result := icmd.RunCmd(cmd)
+
+	result.Assert(t, icmd.Success)
+
+	assertLines(t, result.Stderr(), map[int]compareFunc{})
+
+	assertLines(t, result.Stdout(), map[int]compareFunc{
+		0: equals(`rm s3://%v/abc*/file.txt`, bucket),
+	})
+
+	// ensure files which is not supposed to be deleted.
+	for filename, content := range nonDeletedFiles {
+		assert.Assert(t, ensureS3Object(s3client, bucket, filename, content))
+	}
+}
+
+func TestRemoveS3PrefixRawFlag(t *testing.T) {
+	t.Parallel()
+
+	bucket := s3BucketFromTestName(t)
+
+	s3client, s5cmd, cleanup := setup(t)
+	defer cleanup()
+
+	createBucket(t, s3client, bucket)
+
+	filesToContent := map[string]string{
+		"abc*/file.txt":  "this is a test file 1",
+		"abc*/file1.txt": "this is a test file 2",
+		"abc*/file.py":   "test file 1 python version",
+		"abc*/file.c":    "test file 1 c version.",
+		"abcd/file.txt":  "this is a test file with different prefix",
+	}
+
+	for filename, content := range filesToContent {
+		putFile(t, s3client, bucket, filename, content)
+	}
+
+	cmd := s5cmd("rm", "-raw", "s3://"+bucket+"/abc*")
+	result := icmd.RunCmd(cmd)
+
+	result.Assert(t, icmd.Success)
+
+	assertLines(t, result.Stderr(), map[int]compareFunc{})
+
+	assertLines(t, result.Stdout(), map[int]compareFunc{
+		0: equals(`rm s3://%v/abc*`, bucket), // It prints but does not delete.
+	})
+
+	// all of the files should be in S3
+	for filename, content := range filesToContent {
+		assert.Assert(t, ensureS3Object(s3client, bucket, filename, content))
+	}
+}
diff --git a/storage/url/url_test.go b/storage/url/url_test.go
index e1b20fb..0f30732 100644
--- a/storage/url/url_test.go
+++ b/storage/url/url_test.go
@@ -402,3 +402,33 @@ func TestURLIsBucket(t *testing.T) {
 		}
 	}
 }
+
+func TestURLWithMode(t *testing.T) {
+	tests := []struct {
+		input          string
+		raw            bool
+		prefixExpected string
+		filterExpected string
+	}{
+		{"s3://bucket/file*.txt", false, "file", "*.txt"},
+		{"s3://bucket/file*.txt", true, "", ""},
+		{"s3://bucket/abc/deneme*.txt", false, "abc/deneme", "*.txt"},
+		{"s3://bucket/abc/deneme*.txt", true, "", ""},
+		{"deneme*.txt", false, "deneme", "*.txt"},
+		{"deneme*.txt", true, "", ""},
+	}
+	for _, tc := range tests {
+		url, err := New(tc.input, WithRaw(tc.raw))
+		if err != nil {
+			t.Errorf("There is an error in %s\n", tc.input)
+		}
+
+		if url.Prefix != tc.prefixExpected {
+			t.Errorf("%s : url prefix %s does not match with expected %s\n", tc.input, url.Prefix, tc.prefixExpected)
+		}
+
+		if url.filter != tc.filterExpected {
+			t.Errorf("%s: url filter %s does not match with expected filter %s\n", tc.input, url.Prefix, tc.filterExpected)
+		}
+	}
+}