且听疯吟 在此记录扯淡的青春

由于在 linux 和 windows 之间切换,而且团队中大家用的平台不一样,导致了一些 git 仓库里的旧代码 换行 crlf / lf 不一致,甚至有的文件还是 utf-8 with BOM,所以写了个 powershell 脚本方便直接转换所有文件到 utf-8 和 lf 换行。

网上也有很多类似的命令,但是基本上都有一些小问题。
比如不过滤文件类型导致转换了不该转换的文件;
比如使用 powershell 的 Set-Content 来写文件,导致文件是 utf-8 with BOM;
比如文件末尾的 new line 不正确等等。

script

function Convert-Files($source, $dest) {
    Get-ChildItem -File -Recurse |
        Where-Object {$_.extension -match "^.(py|js|css|html)$"} |
        ForEach-Object {
        Convert-File $_.FullName $source $dest
    }
}

function Convert-File($path, $source, $dest) {
    $utf8NoBomEncoding = New-Object System.Text.UTF8Encoding($False)
    Write-Host "converting:", $path -ForegroundColor "green"
    $x =[System.IO.File]::ReadAllText($path)
    $content = $x -replace $source, $dest
[System.IO.File]::WriteAllText($path, $content, $utf8NoBomEncoding)
}

function dos2unix($file) {
    if ($file) {
        Convert-File $file "`r`n" "`n"
    }
    else {
        Convert-Files "`r`n" "`n"
    }
}

function unix2dos($file) {
    if ($file) {
        Convert-File $file "`n" "`r`n"
    }
    else {
        Convert-Files "`n" "`r`n"
    }
}

copy 到你的 powershell profile 中就可以用了

也可以从这个 gist 地址获取最新版本

食用方法:

  • 过滤文件类型

    修改 Where-Object {$_.extension -match "^.(py|js|css|html)$"} 中的 pattern 来选择需要转换的文件类型

  • 转换所有文件

    直接 cd 到所在目录运行 dos2unix

  • 转换单个文件:

    dos2unix $filepath

💡 因为是直接覆写原文件,虽然我已经测试过很多次了,但是使用前请依然做好提交或备份~

其他

虽然 Github 推荐设置 autocrlf = true,我觉得还是设置 autocrlf = false, safecrlf = true,并通过统一编辑器设置(比如 editorconfig)来保证使用一致的文件和换行格式,避免出现大片的换行和 crlf 的 diff。
毕竟除了 notepad.exe,也没有什么不能识别 lf 换行的“编辑器”了吧~

最近沉迷于 vscode 和 powershell 不能自拔,真的是太好用了~
顺便撸了一个小功能,用来直接在 powershell 中用浏览器打开对应 git repository 的地址

食用方法:

  • 在 powershell 中输入 code $PROFILE 来编辑 profile (或者你也可以使用其他的编辑器~
  • 将以下内容添加到 profile 文件结尾并保存

    function Open-GitWeb {
        $r = git remote -v | Select-String -Pattern "(https:\/\/|git@)(?<git>.*)\.git"
        if ($r.Matches.Length -gt 0) {
            $t = "https://" + ($r.Matches[0].Groups |
                Where-Object {$_.Name -eq "git"}).Value.Replace(":", "/")
            Write-Host "gh: openning ",$t,"..." -ForegroundColor "green"
            Start-Process $t
        }
        else
        {
            Write-Host "gh: not a git repository or origin not set correctly." -ForegroundColor "red"
        }
    }
    
    Set-Alias gh Open-GitWeb
    

    也可以从这个 gist 地址获取最新版本

  • 在 powershell 中输入 . $PROFILE 刷新配置文件(类似于 bash 的 source)

  • done! 在 git repository 目录下输入 gh 就可以打开对应的 url 了

问题

先看下面一个简单的 ASP.NET MVC 5 的 demo:

  • model

    public class TestModel
    {
        public List<int> Ints { get; set; }
    }
    
  • controller

    public ActionResult Index()
    {
        var testModel = new TestModel();
        return View(testModel);
    }
    
    [ActionName("Index"), HttpPost]
    public ActionResult Post(TestModel testModel)
    {
        return View(testModel);
    }
    
  • view

    @model Test.Controllers.TestModel
    
    <form action="@Url.Action("Index")" method="post">
    
        @for (var i = 0; i < 10; i++)
        {
            @Html.TextBoxFor(model => model.Ints[i])
        }
    
        <input type="submit" value="Submit" />
    </form>
    

有没有看出什么问题?

View 里面的

@for (var i = 0; i < 10; i++)
{
    Html.TextBoxFor(model => model.Ints[i])
}

Model.Ints 并没有初始化的情况下被使用了。

正常情况下可能会这么写:

@{
    if (Model.Ints == null)
    {
        Model.Ints = new List<int>();
    }
    for (var i = 0; i < Model.Ints.Count; i++)
    {
        @Html.TextBoxFor(model => model.Ints[i])
    }
}

如果我们需要 10 个 input,可能还得费心给 Model.Ints 初始化并添加 10 个 元素。

然而前面的写法真的会报错吗?

其实并不会,it works well.

为什么呢? @Html.TextBoxFor(model => model.Ints[i])Model.Ints 并未初始化的时候就使用了,那么应该会抛出异常才对?

原因

那么,我们来看看为什么没有报错。
这就要从源代码上找原因。

幸好,ASP.NET MVC 已经在 Github 上开源了,地址在这里

  • 我们很容易根据 namespace 找到 Html.TextBoxFor 的实现,参考 https://github.com/ASP-NET-MVC/aspnetwebstack/blob/master/src/System.Web.Mvc/Html/InputExtensions.cs#L425

  • 简略的说,根据方法签名追踪,可以找到 InputHelper 方法,即https://github.com/ASP-NET-MVC/aspnetwebstack/blob/master/src/System.Web.Mvc/Html/InputExtensions.cs#L483

  • 重点在这一段:

    string attemptedValue = (string)htmlHelper
        .GetModelStateValue(fullName, typeof(string));
    tagBuilder.MergeAttribute("value", attemptedValue ?? ((useViewData) 
        ? htmlHelper.EvalString(fullName, format) 
        : valueParameter), isExplicitValue);
    

    如果要报错,那么应该报错在 htmlHelper.GetModelStateValue,因为很明显这是获取 Model.Ints[i] 的值的地方

  • 继续找到 HtmlHelper.GetModelStateValue 方法,即 https://github.com/ASP-NET-MVC/aspnetwebstack/blob/master/src/System.Web.Mvc/HtmlHelper.cs#L391

    internal object GetModelStateValue(string key, Type destinationType)
    {
        ModelState modelState;
        if (ViewData.ModelState.TryGetValue(key, out modelState))
        {
            if (modelState.Value != null)
            {
                return modelState.Value.ConvertTo(destinationType, null / culture /);
            }
        }
        return null;
    }
    

    重点就在于 ViewData.ModelState.TryGetValue 了,显然 ModelState 主结构是一个 Dictionary 来存储所有的值,这个想必大部分人都知道,所以我们绕了一圈最终找到了这里
    也就是说,实际上是通过 Dictionary.TryGetValue(key, out value) 这样的形式来获取对应的值
    具体到我们的问题,即 i == 0 时,在 ModelState 中寻找 key == "Ints[0]" 的值,当然,其值为 null 并且并不会报错

所以整个流程中并不会因为 Model.Ints 未初始化而报错,因为 Html.TextBoxFor(model => model.Ints[i]) 并不是通过直接访问而是从 expression 数据结构和 ModelState 数据绑定中取值。虽然这背后机制并不复杂,但是这个问题突然冒出来的时候,没有完整看过这部分实现,我也并没有想到这其中的关联。

最后,在使用之前初始化一定是一个好习惯!

附送

其实比起看源码,通过 Visual Studio 来 debug 可能更方便。

那么步骤如下:

  • 找到 Tool -> Options -> Debugging -> General
  • Uncheck Enable Just My Code
  • Check Enable Source Server Support
  • 转到 Tool -> Options -> Debugging -> Symbols
  • Check Microsoft Symbol Servers
  • Add http://referencesource.microsoft.com/symbols
  • Add http://msdl.microsoft.com/download/symbols
  • Add http://srv.symbolsource.org/pdb/Public
  • 我也不知道哪个 symbol server 对你有效,所以就都加上吧~
  • 如果你只需要一部分的 modules,可以选择 Only specified modules,比如添加 System.Web.Mvc.dll

接下来进入调试时,只要右键在当前断点上选择 Step Into Specific 就可以选择进入调试源码了~