0

I have multiple text (.cs) files that all have a line similar to this:

public partial class ApiIThis :  IEquatable<ApiIThis>

or

public partial class ApiIThat :  IEquatable<ApiIThat>

I need to transform them into

public partial class ApiIThis :  IEquatable<ApiIThis>, IThis
public partial class ApiIThat :  IEquatable<ApiIThat>, IThat

This will be run in a .bat file that needs to run in a Windows command line environment

Update: To answer @aschipfl

What have you tried so far, what do you have problems with? Please share your efforts by adding the information to the question!

What I have tried so far is doing some google searching to find some examples of what I'm trying to do. I found an SO post (How to replace substrings in windows batch file) that seems somewhat related, but it wasn't enough to get me going.

Anyway, do the .cs files only have lines like you showed, or even one only?

No, these will not be the only lines in the files. These files will have the full C# class auto-generated by a Swagger tool.

Do you need to find them between other lines, and if so, by what criteria?

Yes they will be between other lines. However, it will be the only line like this in the file. The file has the definition like:

using Something;
using SomethingElse;

public partial class ApiIThis :  IEquatable<ApiIThis>
{
    public ApiIThis() 
    { 
        /* Other code here */ 
    }

    /* Other code here */ 
}

Do the words after class and in between always match?

Yes, they will always match.

Could the words contain white-spaces?

No, they will never contain white-space.

Do you want to collect the transformed lines in a single output file, or do you want a separate output file per input file? or do you even want to overwrite/replace the original files?

I want the original file to be updated.

Finally I wanted to add that they will all start with Api, actually ApiI

Community
  • 1
  • 1
Serj Sagan
  • 28,927
  • 17
  • 154
  • 183
  • 1
    What have you tried so far, what do you have problems with? please share your efforts by adding the information to the question! Anyway, do the `.cs` files only have lines like you showed, or even one only? do you need to find them between other lines, and if so, by what criteria? do the words after `class` and in between `<`/`>` always match? could the words contain white-spaces? do you want to collect the transformed lines in a single output file, or do you want a separate output file per input file? or do you even want to overwrite/replace the original files? – aschipfl May 12 '16 at 19:29
  • Please see update. – Serj Sagan May 12 '16 at 22:03
  • What do you mean with the last sentence: "Finally I wanted to add that they will all start with `Api`, actually `ApiI`"? do you want the file names to be changed by preceding `ApiI`? – aschipfl May 17 '16 at 07:59
  • See how in my example the class `ApiIThis` starts with `ApiI`? That what I mean. No I don't care much about the file names. – Serj Sagan May 18 '16 at 17:11
  • So the lines of interest could also be `public partial class HeyThere : IEquatable` and `public HeyThere()`, and you want the firs tone to be changed to `public partial class ApiIHeyThere : IEquatable` and `public ApiIHeyThere()`, respectively? is that true? – aschipfl May 18 '16 at 18:52
  • This would conflict with your other requirements, where you want to search for the `ApiIHeyThere : IEquatable` portion, extract the string `ApiIHeyThere` and append it without the `Api` prefix (so `, IHeyThere`); also note that most of the current answers are based on the fact that there is `Api` prefixed... so this is another topic and worth a new question in case you tried to accomplish that and failed at some point, at least in my opinion, don't you think? – aschipfl May 18 '16 at 18:54
  • So... 1) this issue is already resolved. 2) No, you have completely missed my requirements. Neither of your comments make any sense. All I want is to turn `public partial class HeyThere : IEquatable` into `public partial class HeyThere : IEquatable : IHeyThere` – Serj Sagan May 18 '16 at 20:13
  • I still don't get it, sorry... anyway, the question is solved; if there is another issue to be solved, consider to ask a new question, given that you did some own research to show and that you provide enough information and sample data so that it can easily be understood... – aschipfl May 18 '16 at 22:05

3 Answers3

2
@echo off
setlocal EnableDelayedExpansion

rem Process all *.cs files
for %%a in (*.cs) do (
   echo Processing "%%a"

   rem Get the number of the target line
   for /F "delims=:" %%b in ('findstr /N /C:"public partial class" "%%a"') do set /A "skip=%%b-1"

   rem Read from original file
   < "%%a" (

      rem Copy previous lines
      for /L %%i in (1,1,!skip!) do (
         set "line="
         set /P "line="
         echo/!line!
      )

      rem Process target line
      set /P "line="
      for /F "tokens=4,6" %%b in ("!line!") do (
         set "Api=%%b"
         echo public partial class %%b :  %%c, !Api:~3!
      )

      rem Copy rest of lines
      findstr "^"

   rem Write to new file
   ) > "%%~Na.new"

   move /Y "%%~Na.new" "%%a"
)

EDIT: I tested this solution with the files provided by the OP. The surprise was that such files does NOT end in CR+LF pairs, but just in LF (Linux style), so they must be converted into CR+LF standard in order to be processed by a Batch file (that uses Windows standard).

I converted the files using more command this way:

for %a in (*.cs) do more %a > %~Na.new
del *.cs
ren *.new *.cs

After that the conversion was correct, excepting for the 4 spaces at beginning of the line that may be directly inserted in the corresponding echo command.

C:\> test.bat
Processing "ApiIClaim.cs"
Processing "ApiIClaimType.cs"
Processing "ApiIMonetaryType.cs"


C:\> for %a in (*.cs) do @fc %a %~Na.new
Comparando archivos ApiIClaim.cs y APIICLAIM.NEW
***** ApiIClaim.cs
    [DataContract]
    public partial class ApiIClaim :  IEquatable<ApiIClaim>
    {
***** APIICLAIM.NEW
    [DataContract]
public partial class ApiIClaim :  IEquatable<ApiIClaim>, IClaim
    {
*****

Comparando archivos ApiIClaimType.cs y APIICLAIMTYPE.NEW
***** ApiIClaimType.cs
    [DataContract]
    public partial class ApiIClaimType :  IEquatable<ApiIClaimType>
    {
***** APIICLAIMTYPE.NEW
    [DataContract]
public partial class ApiIClaimType :  IEquatable<ApiIClaimType>, IClaimType
    {
*****

Comparando archivos ApiIMonetaryType.cs y APIIMONETARYTYPE.NEW
***** ApiIMonetaryType.cs
    [DataContract]
    public partial class ApiIMonetaryType :  IEquatable<ApiIMonetaryType>
    {
***** APIIMONETARYTYPE.NEW
    [DataContract]
public partial class ApiIMonetaryType :  IEquatable<ApiIMonetaryType>, IMonetaryType
    {
*****
Aacini
  • 65,180
  • 12
  • 72
  • 108
  • Sadly, this isn't working. You can see some of the files here: https://www.dropbox.com/s/7u0ugmu5lxwdt8r/ApiIClaim.zip?dl=0 When running your script against them, the , `IWhatever` doesn't get applied but also some other areas of the file are affected. I replaced your `*.cs` with `C:\My\Path\*.cs` which is different from the path where the script is located. – Serj Sagan May 13 '16 at 16:31
  • getting this error now: The following usage of the path operator in batch-parameter substitution is invalid: %~Na.new For valid formats type CALL /? or FOR /? The syntax of the command is incorrect. – Serj Sagan May 13 '16 at 21:03
  • For some reason adding your `newline fix` didn't work... I ended up using this: http://stackoverflow.com/a/17580505/550975 But after that it all worked great. Thanks!!! – Serj Sagan May 16 '16 at 20:22
  • Your script fails as soon as there is more than one space between the words `public`, `partial`, `class`, or if there are tabs instead of spaces; also leading `=` may be a problem for your approach due to `set /P`... – aschipfl May 17 '16 at 06:29
  • ...and what happens if an already processed file is processed again? – aschipfl May 17 '16 at 06:35
1

The following script -- let us call it append-keyword.bat -- does what you want. To use it, provide all the files to process as command line arguments; wildcards ? and * are also allowed:

@echo off
setlocal EnableExtensions DisableDelayedExpansion

set "TMPF=%TEMP%\%~n0_%RANDOM%.tmp"
:LOOP
set "ARGF=%~1"
if defined ARGF (
    setlocal EnableDelayedExpansion
    for %%F in ("!ARGF!") do (
        endlocal
        if /I not "%%~fF"=="%~f0" (
            set "FILE=%%~fF"
            setlocal EnableDelayedExpansion
            if exist "!FILE!" if not exist "!FILE!\" (
                rem /* (it's a file but not a dir. due to "\") */
                > "!TMPF!" call :PROCESS MOV "!FILE!"
                if defined MOV (
                    > nul move /Y "!TMPF!" "!FILE!"
                ) else (
                    > nul del "!TMPF!"
                )
            )
        ) else (
            setlocal EnableDelayedExpansion
        )
    )
    endlocal
    shift /1
    goto :LOOP
)

endlocal
exit /B


:PROCESS  return  item
setlocal DisableDelayedExpansion
set "ITEM=%~2"
set "RTN="
setlocal EnableDelayedExpansion
for /F delims^=^ eol^= %%L in ('findstr /N /R "^^" "!ITEM!"') do (
    endlocal
    set "LINE=%%L"
    set "SKIP="
    rem // Default delimiters TAB and SPACE:
    setlocal EnableDelayedExpansion
    for /F "eol=/ tokens=1-3,*" %%A in ("!LINE:*:=!") do (
        endlocal
        if "%%A %%B %%C"=="public partial class" (
            rem // Delimiters `:`, TAB and SPACE:
            for /F "eol=/ tokens=1,* delims=:    " %%E in ("%%D") do (
                rem // Delimiters `<`:
                for /F "eol=/ tokens=1,* delims=<" %%G in ("%%F") do (
                    if "%%G"=="IEquatable" (
                        rem // Delimiters `>`, `,`, TAB and SPACE:
                        for /F "eol=/ tokens=1,* delims=>,   " %%I in ("%%H") do (
                            if "%%J"=="" (
                                if "%%E"=="%%I" (
                                    set "STR=%%E"
                                    setlocal EnableDelayedExpansion
                                    if "!STR:~,3!"=="Api" (
                                        echo(!LINE:*:=!, !STR:~3!
                                        endlocal
                                        set "SKIP=#"
                                    ) else (
                                        endlocal
                                    )
                                )
                            )
                        )
                    )
                )
            )
        )
        setlocal EnableDelayedExpansion
    )
    if not defined SKIP (
        echo(!LINE:*:=!
    ) else (
        endlocal
        set "RTN=#"
        setlocal EnableDelayedExpansion
    )
)
endlocal
endlocal & set "%~1=%RTN%"
exit /B

The main routine accomplishes the following tasks:

  • handle the command line arguments, loop through them;
  • resolve wildcards by a for loop;
  • exclude the script itself to be processed;
  • check each file for existence;
  • write the return data of the sub-routine to a temporary file;
  • move the temporary file over the processed file in case of changes;

The sub-routine performs the following actions:

  • read the given file line by line (even empty lines);
  • check for words public, partial, class in this order, not regarding the number of white-spaces in between; (regard that it is not checked whether the words are followed by a colon!)
  • check for word IEquatable, followed by <, some more text and >;
  • check if the above text is followed by something else (if yes, the line/file has already been processed, so skip it);
  • check if the words after class and in between < and > match; (regard that the latter string must not contain <, > or , on its own!)
  • check if that word begins with Api, cut it off and append the remaining string to the line, separated by , and a SPACE;
  • return (echo) the modified line or the original one, depending on whether all of the above checks succeed;
  • return a flag indicating whether or not any line has been altered;
  • conduct all string comparisons in a case-sensitive manner;
aschipfl
  • 33,626
  • 12
  • 54
  • 99
  • Sadly, this isn't working. You can see some of the files here: https://www.dropbox.com/s/7u0ugmu5lxwdt8r/ApiIClaim.zip?dl=0 When running your script against them, nothing seems to happen. I named your script InterfaceIt.bat and pass the path which is different from the path of the script like this: IntefaceIt C:\My\Path\*.cs – Serj Sagan May 13 '16 at 16:30
  • That was supposed to be IntefaceIt C:\My\Path\\*.cs – Serj Sagan May 13 '16 at 17:08
  • What is the edcoding of the `*.cs` files? what line endings are used? – aschipfl May 13 '16 at 17:17
  • I think it is the standard windows text file stuff. If I had to guess, UTF8 and /r/n – Serj Sagan May 13 '16 at 17:25
  • Your files have Unix-style end-of-line codes (\n), although you are working on Windows, but that is not a problem for my script. However, I found another stupid mistake, so that leading white-spaces at the line containing `public partial class` caused the script to fail; anyway, now it should work as expected... – aschipfl May 17 '16 at 06:21
-1

Filtering (with PowerShell)

Here's a script that uses PowerShell (comes with all Windows since Vista, installed on XP via an update).

Param(
    [String]
    $input_file
)

Function Filter-Data
{
    Param(
        [Parameter(ValueFromPipeline=$True)]
        [String]
        $line
    )
    Process {
        if ($line -match "public\s+partial\s+class\s+ApiI([A-Za-z0-9_]+)\s*:\s*IEquatable\s*<\s*ApiI[A-Za-z0-9_]+\s*>") {
            Return $matches[0] + ", " + $matches[1]
        } Else {
            Return $line
        }
    }
}

Get-Content $input_file | Filter-Data | Write-Host

You give it the input file name. It spits out the filtered data. You may want to make the filtering more intelligent (allowing spaces, etc.), but what's here will do the trick.

Invocation might look like this:

PowerShell -NoProfile -ExecutionPolicy Bypass -File filter.ps1 source_file.cs <NUL
Notes
  • -NoProfile will tell PoSH not to load the user's profile (faster startup)
  • -ExecutionPolicy must allow files, and if you haven't configured one on your machine, you'll want to specify ByPass so that PoSH will run the script.
  • <NUL PowerShell frequently waits for you to hit ENTER before completing execution. Redirecting nothing in will return control to the caller as soon as the script is done.

EDIT based on further detail added to the question and the comment about triviality, I updated the filtering code to match and replace more generally. It also allows spaces between C# tokens, even though that wasn't an official requirement (I don't wholly trust code generators).

mojo
  • 4,050
  • 17
  • 24
  • 2
    The question was giving **examples** of patterns to search for. Those aren't to be taken literally, like your script does. This is not an answer to the question. – IInspectable May 12 '16 at 19:26
  • This script could easily be adapted to meet any kind of matching needs. PowerShell at least uses regular expressions, unlike the single-character string splitting that cmd is capable of. Change the `-match` expressions and you can make it do whatever you want. Was the question, "How do I match strings like these?" or was it "How do I do this operation on my files?" This is an answer to the second one. – mojo May 13 '16 at 11:42
  • Correct, and the second question is the trivial one. The hard one (matching a pattern where a yet to be discovered token needs to show up in another place) is left unanswered, and anything but trivial (`-match` won't be of much help). If you believe that this is a matter of applying a simple regular expression, you are invited to improve your answer. – IInspectable May 13 '16 at 11:54
  • Anything is trivial if you already know how to do it. Parsing/matching in cmd: not the least bit trivial, and very painful to maintain. – mojo May 13 '16 at 14:01
  • Your proposed solution will match `public partial class ApiIThis : IEquatable` as well. This does not meet the specification. And it fails to account for C# spacing rules as well, although that wasn't specifically requested either (e.g. `... ApiIThis:IEquatable ...` or `... IEquatable < ApiIThat > ...`. – IInspectable May 13 '16 at 14:47
  • You are correct. I couldn't find a good way to use grouping to find identical strings with PoSH regex. However, I don't think that will prevent the solution from being useful. We're talking about generated code. It's not going to This-That. It's going to This-This and That-That. My goal was to make a short, readable solution that could be customized by the OP to serve his purposes. 90 lines of batch, though perhaps entirely functional, is difficult to debug should one need to adapt it to some non-identical purpose. – mojo May 16 '16 at 13:16
  • The OP's not paying for a complete solution, he wants help. I've provided a brief, functional example of something he could use to solve his problem. I'm not shooting for bulletproof. I'm after helpful. – mojo May 16 '16 at 13:17
  • Alright then, lesson learned: Next time I'll simply vote on a question without leaving a comment. It always leads to tedious discussions on why wrong isn't right. – IInspectable May 16 '16 at 13:26
  • The lesson could be that right/wrong isn't even the criteria being measured. "Useful" is what's being measured. – mojo May 16 '16 at 13:40
  • An answer that is wrong cannot possibly be helpful (unless in the short term, maybe, until things break spectacularly). I earn most of my money because developers tried to be helpful, rather than correct. The cost for fixing bugs **is** a measurable criteria. (In case you are wondering: Stackoverflow is for professional developers as well.) – IInspectable May 16 '16 at 13:47
  • The OP only gave a template for what was wanted. My solution is meant to be customized based on the (unstated) particulars of what is needed. It's a framework, not a final solution. It adequately demonstrates *how* to get the job done. Anyone who uses it should definitely change the expression (to handle spaces or whatever), or add a few more to handle what's needed. – mojo May 16 '16 at 17:05
  • Are you honestly going to propose that string handling in batch is the *right* way to solve a problem? Is it going to save time in maintenance or debugging? I accept your solution as useful, conceding functional correctness (I didn't attempt to verify it). Why is a perfectly simple and adaptable solution *wrong* because it doesn't take into account the details (some of which are unstated)? Would you rather debug 90 lines of batch with nested for loops or a single regular expression? – mojo May 16 '16 at 17:10
  • No clue, what deluded observation of reality led you to believe, that I would propose to use the Windows batch file language for anything, when I could use PowerShell. My issue is, that you keep arguing that half of a solution (as proposed by you) is better than no solution at all. Stackoverflow is not the place where you post answers where you "have to work out the details". Particularly when those details are the gist of it. As always, well meant and well done are orthogonal. If you feel like continuing this useless debate, go ahead, and have the last word. My time does have value, I'm out. – IInspectable May 16 '16 at 17:32
  • http://meta.stackoverflow.com/questions/255543/is-it-acceptable-to-leave-out-implementing-some-details-of-a-problem-solution – mojo May 16 '16 at 18:26
  • 1
    *"There's "partial" in the sense that you have a 90% solution the explains the salient issues but does not deal with edge cases, and then there's "partial" in the sense of having a one-liner that merely sends the OP in the right direction but explains nothing. The second case is more likely to get a cold reception."* Your answer is the second case. You didn't leave out a detail. You left out the bulk. You cannot answer a question about pattern matching that includes how to iterate over files, but leaves the precise regular expression to the OP "to be worked out". – IInspectable May 16 '16 at 21:04