Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

How to name (or rename) files based on a parameter? #1238

Closed
sstchur-zz opened this issue Sep 12, 2017 · 17 comments

Comments

@sstchur-zz
Copy link

commented Sep 12, 2017

I've been unable to find any way to ensure that a file in my template project ends up with a specific name.

Note that I'm not talking about sourceName. I understand how that works, and it works just fine for what it is.

Rather, I'm trying to do the following: suppose I have a file called MyService.cs in my project. And further suppose that I have a parameter defined in my template.json as:

"symbols":
{
	"serviceName":
	{
		"type": "parameter",
		"defaultValue": "MyService",
		"replaces": "__MYSERVICE__"
	}
}

When the user runs: dotnet new MyTemplate -s FuzzyMonkey

I want the file MyService.cs to be renamed FuzzyMonkey.cs

I can make the actually class name inside of MyService.cs get renamed easily enough. That seems to work if I name the class

public class MYSERVICE
{...}

But it does not seem to rename the file, even if the file is named "MYSERVICE.cs"

Is there any way to do this?

As a bonus, it would be even better if I could default the serviceName parameter not to a hardcoded value, but to a derivative of sourceName. Like:

	"serviceName":
	{
		"type": "parameter",
		"defaultValue": $sourceName.substring(1, 5),
		"replaces": "__MYSERVICE__"
	}

Something like the above, which I just totally made up. But I trust you get the idea. I'd like the default value for serviceName to be derived from the value of sourceName.

@seancpeters

This comment has been minimized.

Copy link
Collaborator

commented Sep 13, 2017

@sstchur - you can setup a parameter with the attribute "FileRename", which will cause files containing the value of "FileRename" to have that value replaced with the provided parameter value. No replacement occurs if the parameter isn't given a value and doesn't have a default. For example:

  "symbols": {
    "RenameParam": {
	  "type": "parameter",
	  "datatype": "string",
	  "description": "Renames files containing 'renameme'",
	  "FileRename": "RenameMe",
	  "defaultValue": "zzz"
    }
    ...
  }

then on the command line, if you invoke the template with --RenameParam NewName the string literal 'RenameMe' will be replaced with 'NewName' for all files in the template.

For your bonus question, we actually recently implemented something very similar, which should hopefully fit your needs. It's a new type of symbol called "derived". This allows setting up a symbol whose value is derived from the value of another symbol using what we call value forms. Check out this issue, which was the inspiration for derived symbols #1066 and this PR which implements it #1194. The issue discussion provides an example of setting up something like this, and the PR contains unit tests demonstrating its usage. Derived symbol types have pretty much the same options as parameters symbols (plus deriving their values).

This feature is not available in the 2.0.0 release of dotnet, but is available in the 2.1.0 preview branch of this repo.

Let me know if the usage is unclear, or if you need help with your specific use case.

@sstchur-zz

This comment has been minimized.

Copy link
Author

commented Sep 13, 2017

This works! Thanks. I haven't tried the preview version yet to get the derived symbols working, but the FileRename attribute worked like a charm.

@sstchur-zz sstchur-zz closed this Sep 13, 2017
@sstchur-zz sstchur-zz reopened this Sep 14, 2017
@sstchur-zz

This comment has been minimized.

Copy link
Author

commented Sep 14, 2017

I just noticed what seems to be a bug -- perhaps it should be logged as a separate issue if indeed it is a bug?

It seems that if you have multiple symbols with attribute "FileRename", only the first is honored and subsequent ones are ignored. For example:

	"appName":
	{
		"type": "parameter",
		"datatype": "string",
		"isRequired": true,
		"replaces": "SFAPP1",
		"fileRename": "SFAPP1"
	},

	"serviceName":
	{
		"type": "parameter",
		"datatype": "string",
		"isRequired": true,
		"replaces": "SFSERVICE1",
		"fileRename": "SFSERVICE1"
	}

In the above, only SFAPP1 will be replaced. SFSERVICE1 will remain untouched, even if when supplied via command line.

Also, if I flip the parameters so that SFSERVICE1 comes first, then only it will be replaced, and all occurrences of SFAPP1 will remain untouched.

Is this a bug? It feels like one to me.

@seancpeters

This comment has been minimized.

Copy link
Collaborator

commented Sep 14, 2017

I would agree that this is a bug... I'll look into it and get back to you.

@seancpeters

This comment has been minimized.

Copy link
Collaborator

commented Sep 15, 2017

@sstchur - I'm unable to repro this issue using dotnet 2.0.0 (or newer).

Using a template with the above [partial] configuration, if you run a command such as:
dotnet new <templateName> --appName foo --serviceName bar
then the expectation is that any filenames containing SFAPP1 will have that string replaced with foo and any filenames containing SFSERVICE1 will have that string replaced with bar.

The renames are case-sensitive (because case matters for filenames on some systems, notably *nix). But with the behavior you're seeing, that is unlikely to be the issue.

Is the problem with a file whose name contains both of the strings you're trying to replace? For example a file named SFAPP1_SFSERVICE1.txt? That situation I can repro.

@sstchur-zz

This comment has been minimized.

Copy link
Author

commented Sep 15, 2017

So, I am in fact trying to rename something that contains both strings, and initially I thought that was the problem. However, my project/template has both: occurrences of SFSERVICE1 by itself that aren't getting renamed, AND occurrences of SFAPP1 along with SFSERVICE1 that aren't getting renamed.

If the problem were only when both strings occur together, shouldn't at least the occurrences of SFSERVICE1 alone be working? It works when I flip the order of parameters in my template.json file.

For example, my project has:

Folder: SFAPP1 // this gets renamed
Folder: SFAPP1.Services.SFSERVICE1 // only SFAPP1 gets renamed here
File: SFAPP1.sln // this get renamed

Under folder SFAPP1.Services.SFSERVICE1
File: SFAPP1.Services.SFSERVICE1.csproj // only SFAPP1 gets renamed here
File: SFSERVICE1.cs // does NOT get renamed

And then, there are many text occurrences of SFSERVICE1 inside of .cs and .json files that do not get renamed, but all occurrences of SFAPP1 do.

Not sure what I'm doing wrong? My parameters are defined in my template.json exactly as I pasted in my previous comment.

By the way: I am using version 2.0.0 according to dotnet new --version

@seancpeters

This comment has been minimized.

Copy link
Collaborator

commented Sep 15, 2017

@sstchur - you're absolutely correct, what you're describing should not be happening. Would you be able to zip up your template and send me a copy to help diagnose the problem? If there's proprietary IP or sensitive information, those parts could be removed.

My email is: sepeters@microsoft.com

@sstchur-zz

This comment has been minimized.

Copy link
Author

commented Sep 15, 2017

Yes, I will contact you directly, thanks.

@seancpeters

This comment has been minimized.

Copy link
Collaborator

commented Sep 16, 2017

@sstchur - I've had a chance to look at your template, and have unfortunately discovered a couple bugs in the templating code, fixes should be coming in the next few days.

On a side note, I noticed that in ServiceEventSource.cs, there are some preprocessor directives. That is fine, but due to the way templating deals with conditionals vs. preprocessor directives, we require the conditions to be inside parentheses. They sometimes happen to work without parentheses, but this is not guaranteed to be supported - in fact, they don't work properly in 2.1.0-preview1. This should be a minor, painless change.

For the file content replacements, the only potential replacements I saw which didn't get properly replaced are due to casing differences. The file content replacements are case sensitive, so places where a file contained, for example "sfapp1", that string will not be replaced, because the string configured to be replaced is "SFAPP1". For 2.1.0-preview1, we introduced a mechanism where you can setup symbols so that different "forms" of the value being replaced can all be replaced, in additional to the literal "replaces" value (PR #867). In your case, we could set things up so that "SFAPP1" gets replaced by the literal input value of the appName parameter, and also have "sfapp1" be replaced by the lowercased version of the appName input value.

There is a bug in the code to handle this mechanism, but I have a PR which fixes the issue #1241

Setting up the lowercase replacements is configured on the parameters, like this:

		"appName":
		{
			"type": "parameter",
			"datatype": "string",
			"isRequired": true,
			"replaces": "SFAPP1",
			"fileRename": "SFAPP1",
			"forms": {
			  "global": ["identity", "lowerCase"]
			}
		},

		"serviceName":
		{
			"type": "parameter",
			"datatype": "string",
			"isRequired": true,
			"replaces": "SFSERVICE1",
			"fileRename": "SFSERVICE1",
			"forms": {
			  "global": ["identity", "lowerCase"]
			}
		}

The "forms" indicate the transformations to perform on both the "replaces" value and the user input values, the results of those transforms are used for the replacements. Using "serviceName" as an example, lets say I invoke the template with --serviceName MyService. There are 2 forms defined on service name, "identity", and "lowerCase".

  • The "identity" form leaves the values as-is, so the replaces value of "SFSERVICE1" will be replaced by the input value "MyService".
  • The "lowerCase" form lowercases both values, causing "sfservice1" to be replaced by "myservice".

There was also one place in your template with mixed-case in the source, specifically in HomeController.cs, the namespace beings with "SPApp1". If you want to retain that formatting, we'll need to come up with a custom value form for handling that situation... I'll need to give it more thought. But if that can be changed in your source to "SFAPP1", the file content replacements should be good to go - assuming you use 2.1.0-preview1 or newer, and PR #1241 is merged.

I still need to make a fix for the bug with multiple replacements in a single filename.

If there's other problems you're having which I didn't cover here, or if you have additional questions, please let me know.

@sstchur-zz

This comment has been minimized.

Copy link
Author

commented Sep 16, 2017

Thanks @seancpeters
I think you have covered everything. The "SFApp1" mis-casing you mentioned is just a mistake and I can easily fix that. The lowercase was by design, but as you pointed out, you now have a mechanism for this, so I will try to use parameters as you've described for that.

Point take on the preprocessor directives -- that seems easy enough.

Seems to me that once that fixes are in, all my issues will have been addressed. Thanks!

@seancpeters

This comment has been minimized.

Copy link
Collaborator

commented Sep 19, 2017

PR #1245 takes care of your file rename problem / my file rename bug :)

@seancpeters

This comment has been minimized.

Copy link
Collaborator

commented Sep 19, 2017

@sstchur - The changes necessary for your template to work are now merged into dotnet:rel/2.1.0-preview1.

@CumpsD

This comment has been minimized.

Copy link

commented Mar 26, 2019

any way to have it do camelCasing/PascalCasing renames too? Instead of just lower/upper.

I have this in my template.json:

    "MainAggregate": {
      "type": "parameter",
      "datatype": "string",
      "description": "Main aggregate of the registry, adds an example aggregate, command handler, value objects, exceptions and controller.",
      "defaultValue": "",
      "replaces":"ExampleAggregate"
    },

I supply a value of User on the CLI but I end up with:

        public static User Register(UserId exampleAggregateId)
        {
            var exampleAggregate = Factory();
            exampleAggregate.ApplyChange(new UserWasBorn(exampleAggregateId));
            return exampleAggregate;
        }

Some are correctly replaced, but the camelcased ones aren't touched. I want them to turn into userId for example

Any suggestions if that is possible right now @seancpeters?

@CumpsD

This comment has been minimized.

Copy link

commented Mar 26, 2019

Ok, this is ugly, but it works....

    "MainAggregate": {
      "type": "parameter",
      "datatype": "string",
      "description": "Main aggregate of the registry, adds an example aggregate, command handler, value objects, exceptions and controller.",
      "defaultValue": "",
      "replaces":"ExampleAggregate",
      "fileRename": "ExampleAggregate"
    },
    "MainAggregateCamelCase": {
      "type": "generated",
      "generator": "regex",
      "dataType": "string",
      "replaces": "exampleAggregate",
      "parameters": {
        "source": "MainAggregate",
        "action": "replace",
        "steps": [
          { "regex": "^(A)([a-zA-Z]*)", "replacement": "a$2" },
          { "regex": "^(B)([a-zA-Z]*)", "replacement": "b$2" },
          { "regex": "^(C)([a-zA-Z]*)", "replacement": "c$2" },
          { "regex": "^(D)([a-zA-Z]*)", "replacement": "d$2" },
          { "regex": "^(E)([a-zA-Z]*)", "replacement": "e$2" },
          { "regex": "^(F)([a-zA-Z]*)", "replacement": "f$2" },
          { "regex": "^(G)([a-zA-Z]*)", "replacement": "g$2" },
          { "regex": "^(H)([a-zA-Z]*)", "replacement": "h$2" },
          { "regex": "^(I)([a-zA-Z]*)", "replacement": "i$2" },
          { "regex": "^(J)([a-zA-Z]*)", "replacement": "j$2" },
          { "regex": "^(K)([a-zA-Z]*)", "replacement": "k$2" },
          { "regex": "^(L)([a-zA-Z]*)", "replacement": "l$2" },
          { "regex": "^(M)([a-zA-Z]*)", "replacement": "m$2" },
          { "regex": "^(N)([a-zA-Z]*)", "replacement": "n$2" },
          { "regex": "^(O)([a-zA-Z]*)", "replacement": "o$2" },
          { "regex": "^(P)([a-zA-Z]*)", "replacement": "p$2" },
          { "regex": "^(Q)([a-zA-Z]*)", "replacement": "q$2" },
          { "regex": "^(R)([a-zA-Z]*)", "replacement": "r$2" },
          { "regex": "^(S)([a-zA-Z]*)", "replacement": "s$2" },
          { "regex": "^(T)([a-zA-Z]*)", "replacement": "t$2" },
          { "regex": "^(U)([a-zA-Z]*)", "replacement": "u$2" },
          { "regex": "^(V)([a-zA-Z]*)", "replacement": "v$2" },
          { "regex": "^(W)([a-zA-Z]*)", "replacement": "w$2" },
          { "regex": "^(X)([a-zA-Z]*)", "replacement": "x$2" },
          { "regex": "^(Y)([a-zA-Z]*)", "replacement": "y$2" },
          { "regex": "^(Z)([a-zA-Z]*)", "replacement": "z$2" }
        ]
      }
    },
    "MainAggregateCamelCaseField": {
      "type": "generated",
      "generator": "regex",
      "dataType": "string",
      "replaces": "_exampleAggregate",
      "parameters": {
        "source": "MainAggregate",
        "action": "replace",
        "steps": [
          { "regex": "^(A)([a-zA-Z]*)", "replacement": "_a$2" },
          { "regex": "^(B)([a-zA-Z]*)", "replacement": "_b$2" },
          { "regex": "^(C)([a-zA-Z]*)", "replacement": "_c$2" },
          { "regex": "^(D)([a-zA-Z]*)", "replacement": "_d$2" },
          { "regex": "^(E)([a-zA-Z]*)", "replacement": "_e$2" },
          { "regex": "^(F)([a-zA-Z]*)", "replacement": "_f$2" },
          { "regex": "^(G)([a-zA-Z]*)", "replacement": "_g$2" },
          { "regex": "^(H)([a-zA-Z]*)", "replacement": "_h$2" },
          { "regex": "^(I)([a-zA-Z]*)", "replacement": "_i$2" },
          { "regex": "^(J)([a-zA-Z]*)", "replacement": "_j$2" },
          { "regex": "^(K)([a-zA-Z]*)", "replacement": "_k$2" },
          { "regex": "^(L)([a-zA-Z]*)", "replacement": "_l$2" },
          { "regex": "^(M)([a-zA-Z]*)", "replacement": "_m$2" },
          { "regex": "^(N)([a-zA-Z]*)", "replacement": "_n$2" },
          { "regex": "^(O)([a-zA-Z]*)", "replacement": "_o$2" },
          { "regex": "^(P)([a-zA-Z]*)", "replacement": "_p$2" },
          { "regex": "^(Q)([a-zA-Z]*)", "replacement": "_q$2" },
          { "regex": "^(R)([a-zA-Z]*)", "replacement": "_r$2" },
          { "regex": "^(S)([a-zA-Z]*)", "replacement": "_s$2" },
          { "regex": "^(T)([a-zA-Z]*)", "replacement": "_t$2" },
          { "regex": "^(U)([a-zA-Z]*)", "replacement": "_u$2" },
          { "regex": "^(V)([a-zA-Z]*)", "replacement": "_v$2" },
          { "regex": "^(W)([a-zA-Z]*)", "replacement": "_w$2" },
          { "regex": "^(X)([a-zA-Z]*)", "replacement": "_x$2" },
          { "regex": "^(Y)([a-zA-Z]*)", "replacement": "_y$2" },
          { "regex": "^(Z)([a-zA-Z]*)", "replacement": "_z$2" }
        ]
      }
    },

All of this because .NET Regex doesn't allow \L$1$2

@danadesrosiers

This comment has been minimized.

Copy link

commented Jun 3, 2019

The "fileRename" option was exactly what I was looking for. I just want to note that it is not in the JSON Schema. You might want to add that.

@adamjones1

This comment has been minimized.

Copy link

commented Jun 20, 2019

I can only seem to get this working for parameter symbols, could it be extended for other symbol types as well? (Generated ones, particularly.)

@wendellestradairely

This comment has been minimized.

Copy link

commented Sep 18, 2019

How do you parse names with relative paths so that the class name will not be replaced with directory paths? For example, my template is ServiceNameService, then, when you run the command dotnet new --name "Services/Payroll", the file will be created as PayrollService.cs inside a folder named Services however the problem is that the class name also gets renamed to "Services/PayrollService":

template.json

{
    "$schema": "http://json.schemastore.org/template",
    "author": "Me",
    "classifications": [ "Common", "Code" ],
    "identity": "My.Templates.CSharp",
    "name": "MyTemplates",
    "shortName": "mytemplate",
    "tags": {
        "language": "C#",
        "type": "item"
    },
    "symbols": {
        "ServiceName": {
            "type": "parameter",
            "datatype": "text",
            "description": "Renames files containing 'ServiceName'",
            "fileRename": "ServiceName",
            "replaces": "ServiceName",
            "isRequired": true,
            "defaultValue": "MyService"
        }
    }
  }

Template:

public class ServiceNameService
{ 
}

Generated file:

L Project Folder
     L Services
           L PayrollService.cs

The actual class or content of the generated file:

public class Services/PayrollService
{ 
}

Expected:

public class PayrollService
{ 
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
6 participants
You can’t perform that action at this time.