出于娱乐目的,让我们来完成一项我在现实世界中出于政治原因实际上不值得花精力解释的工作。
假设您有一些想要跟踪其更改的源代码,但是由于某些原因,您被禁止使用系统上尚未存在的任何二进制文件,并且唯一可用的二进制文件是busybox。
无法正确跟踪更改并开始构建带时间戳的tar档案,这让他们大哭。
意识到busybox包含了diff和patch,您也许可以使用它们来构建一些东西。它永远不会表现出色或完美无缺,但这可能已经足够了。
就像我最终构建的系统的名称所暗示的那样,shiv足以使自己受益匪浅。对于任何遥不可及的地方,它还不够好,如果您看到有人在使用它,那就赶紧爬山吧。或询问他们是否要分段。
如果您实际使用此产品,则表示您接受了故障,错误以及可能会受到Black Plague亲自感染的可能性,而无视自己的安全,并承认自己是绝对的傻瓜。随着古老的格言...
此软件由版权所有者和贡献者按原样提供。不作任何明示或暗示的保证,包括但不限于针对特定目的的适销性和适用性的暗示保证。在任何情况下,版权所有人或贡献者均不对任何直接,间接,偶发,特殊,专有或后果性的损害(包括但不限于,替代商品或服务的购买,使用,数据,或业务中断),无论是基于合同,严格责任还是侵权(包括疏忽或其他方式)造成的任何责任,无论是否出于使用本软件的任何方式(即使已经事先告知)。
此处讨论的所有代码都应与shiv75相关,并演示创建它的过程。如果您实际上在现实世界中使用此代码-我讨厌您。
我们知道,最终将以补丁文件列表以及其他一些元数据结束。
因此,我们的首要任务是在可以放置它们的地方建造。我们不知道确切的架构,但是我们将非常简单地开始,然后从那里开始进行构建。
#!/ bin / shset -ex#创建一个新的信息库init_shiv(){如果[-d' .shiv' ];然后(&& 2 echo'已经是一个shiv存储库。')退出1 fi mkdir' .shiv'}#CLI parsingif [" $ 1" =' init' ];然后init_shiv退出0else(& 2 echo'未知命令。')退出1fi
这看起来很不错。我们将把各个任务分解为功能,然后稍后处理命令解析以调用这些功能。这将使扩展和重新构造各个组件变得更加容易。
这里已经有一个庞大的步枪:出于全能的神只知道的原因,set -e通常会导致在失败的命令上退出,而在函数内部则无法正常工作。所以...我们将不得不重新架构。
值得庆幸的是,set -x可以打印出引擎盖下发生的事情,几乎在所有地方都以相同的方式工作。
#!/ bin / shset -exshiv_bin =" $(realpath" $ 0")"#初始化新的存储库,如果[" $ 1" =&#39__init' ];那么如果[-d&#39..shiv' ];然后(&& 2 echo'已经是shiv储存库。')退出1 fi mkdir' .shiv' #TODO:形成一棵文件夹树...退出0fi#CLI parsingif [" $ 1" =' init' ];然后" $ shiv_bin" ' _init'退出0else(&& 2 echo'未知命令。')退出1fi
乍一看,将内部暴露为_ *似乎是多余的。但是,我们将拥有其中一些,并且我们不想检查每个内置函数或函数的退出状态。它很脆弱,但是我们正在运行用于版本控制的Shell脚本,因此我们已经不理会了。
要注意的一件事:我们基本上是为每个子命令派生的,因此它也应该有一个退出命令,因此它不会陷入CLI解析中。
我们需要的下一个任务是能够找到.shiv存储库。仅检查它是否在当前工作目录中还不够好。我们还要检查上方的文件夹。
#查找shiv存储库if [" $ 1" =' _find_shiv' ];然后#向上搜索sysroot。 dir =" $(pwd)"而[! -d" $ dir" /。shiv]&& [" $ dir" !=' /' ]; do dir =" $(dirname" $ dir")" #如果我们发现了一些东西,将其发射出去。如果[-d" $ dir" /。shiv];然后回显" $ dir"退出0否则退出1 fifi
这可能是我们可以使用的最重要的命令,并且将在所有地方广泛使用。控制退出状态将使我们可以在if语句中使用它。我们在这里没有做任何真正令人惊讶的事情,我们只是向上递归,直到找到它或点击根目录为止。
#初始化新的存储库if [" $ 1" =&#39__init' ];然后如果" $ shiv_bin" ' _find_shiv' > / dev / null;然后(&& 2 echo'已经是shiv储存库。')退出1 fi mkdir' .shiv' #TODO:形成文件夹树...退出0fi
您可能会或可能不想这样做。这取决于。但是,出于我们的目的,我们不想支持子存储库,因此我们只是禁止它们存在。请记住,我们已经在处理一个高度任意的约束,我们不想破坏自己的力量。
在完成init命令之前,我们还需要做出一些决定。 (当然,没有什么是永久的。这是一个尚未部署的内部项目。您可以不断对其进行更改,直到投入生产为止。)
我们想要分支-分支将是提交列表。然后,大概是平面文件。
我们将希望包括一些最小数据,例如提交时间,以使合并以后的精神状态减少。它们仍然很凌乱,可能会破裂,但至少它们会起作用。
我们希望提交具有元数据。我们需要发明一种格式来存储所需的位:
可能类似于过程命令界面,例如:PATCH:(base64文件名)|(patch_id)和TOUCH:(base64文件名)|(UTC UNIX时代时间戳记)
我们想将补丁存储在某个地方。 (将它们排除在提交之外意味着如果它们相同,我们可以重新使用现有的补丁!)
在提交文件之前,我们需要跟踪要提交的文件。然后,可能只是文件中的文件名列表。
#初始化新的存储库if [" $ 1" =&#39__init' ];然后如果" $ shiv_bin" ' _find_shiv' > / dev / null;然后(&& 2 echo'已经是shiv资料库。')退出1 fi#我们的主要资料库位置mkdir' .shiv' #我们将存储分支文件mkdir&.39.shiv / branches'的位置#默认的分支文件触摸&#39..shiv / branches / master' #我们将存储提交的位置mkdir' .shiv / commits' #我们将补丁存储在哪里mkdir&.39.shiv / patches' #我们将在何处跟踪要提交的内容touch&#39.shiv / staging' #告诉shiv我们在master分支上echo' master' > &#39.shiv /当前'退出0fi
.shiv(D)├──分支(D)│└──主(F)├──提交(D)├──当前(F)├──补丁(D)└──阶段(F)
显而易见的下一步,也是非常简单的一步,是告诉shiv我们要暂存哪些文件进行提交。
如果[" $ 1" =&#39__stage_file' ];那如果! " $ shiv_bin" ' _find_shiv' > / dev / null;然后(&& 2 echo'不是shiv储存库。)exit 1 fi#重写相对于我们储存库根目录的路径。 shiv_root =" $(" $ shiv_bin"' _find_shiv')" file =" $ 2" path =' ./ 39; $(realpath --relative-to =" $ shiv_root"" $ file")" #TODO:将路径添加到暂存中...退出0fi
正是在这一点上,您意识到这并不像看起来那样容易。因为--relative-to标志是GNU扩展,所以我们只能访问busybox。
我们将必须找到一种方法来重写相对于根的路径,以使其对我们完全有用。
简短的答案是,这是一个过程,但值得庆幸的是,其他人以前也曾尝试过。因此,我们可以添加这两个功能来复制大多数功能,并且它应该对我们有用。大多数时候。
#在测试" $ {path#" $的同时,获取相对于另一个... relPath(){本地公用路径,向上common = $ {1%/} path = $ {2%/} /常见" /}" =" $ path&#34 ;;做common = $ {common%/ *} up = .. / $ up完成路径= $ up $ {path#" $ common" /}; path = $ {path%/}; printf%s" $ {path:-。}"}#Readlink要求文件实际上带有-f标志,但是我们没有太多选择。relpath(){ relPath" $(readlink -f" $ 1")" " $(readlink -f" $ 2")&#34 ;; }
既然这样,我们实际上可以开始暂存文件路径了:
#暂存文件if [" $ 1" =&#39__stage_file' ];那如果! " $ shiv_bin" ' _find_shiv' > / dev / null;然后(&& 2 echo'不是shiv储存库。)exit 1 fi#重写相对于我们储存库根目录的路径。 shiv_root =" $(" $ shiv_bin"' _find_shiv')" file =" $ 2" path =' ./ 39; $(relpath" $ shiv_root"" $ file")" #编码路径名enc_path =" $(echo" $ path" | base64 | tr -d' n')" #推送文件名echo" $ enc_path" >> " $ shiv_root" /。shiv / staging#重复数据删除staging_data =" $(sort .shiv / staging | uniq)"回声" $ staging_data" > " $ shiv_root" /。shiv /登台出口0fi
我们对名称进行编码,以防止文件路径中的任何字符弄乱任何内容,并删除重复文件,因为我们打算稍后对其进行迭代。
最后,我们需要添加一些CLI解析以使其能够添加文件:
#CLI parsingif [" $ 1" =' init' ];然后" $ shiv_bin" ' _init'退出0elif [" $ 1" ='添加' ];然后转移#摆脱' add'为" $ @&#34 ;;中的我做" $ shiv_bin" ' _stage_file' " $ i"完成退出0else(&& 2 echo'未知命令')退出1fi
这意味着我们可以运行诸如sh shiv add file_a file_b file_c之类的命令并将它们一次转换。
现在我们可以暂存文件,我们需要能够构建提交。这可能只是该过程中最重要的部分,
#Commitif [" $ 1" =&#39__commit' ];那如果! " $ shiv_bin" ' _find_shiv' > / dev / null;然后(&& 2 echo'不是shiv储存库。')退出1 fi shiv_root =" $(" $ shiv_bin"' _find_shiv' )" cwd =" $(pwd)" cd" $ shiv_root"而读-r行;做filename =" $(echo" $ line" | base64 -d)" #TODO:重建文件的最新版本#TODO:获取新版本的差异并保存到补丁程序中#TODO:完成添加到我们的提交数据中< " $ shiv_root" /。shiv / staging cd" $ cwd"退出0fi
这是事情开始变得有点弯曲的时候,您需要将shiv的整个内部工作保持在脑海中。因此,如果您像我一样遭受慢性疲劳的困扰,那么期望它会花费您一段时间才能成功。
为了生成一个新的提交,例如我们的初始提交,我们首先需要使用管道来从以前的提交中重建文件。
但是,除了diff以外,我们都可以汇编所有内容,这将有助于我们确切地知道我们将如何构造提交文件,因此让我们来做吧...
#Commitif [" $ 1" =&#39__commit' ];那如果! " $ shiv_bin" ' _find_shiv' > / dev / null;然后(&& 2 echo' Not不是shiv存储库。')退出1 fi shiv_root =" $(" $ shiv_bin"' _find_shiv' )" commit_message =" $ 2"如果[-z" $ commit_message" ];然后#注意:相当于“我是个混蛋”。 #但是该消息未能清除管理人员,#谁坚持批准没有#标志的空消息。 commit_message ="没有提交消息。 fi enc_message =" $(echo" $ commit_message" | base64 | tr -d' n')" now =" $(date' +%s')"回声" DATE | $ now" >> " $ commit_file" commit_file =" $(mktemp)"回声" MESSAGE | $ enc_message" > " $ commit_file" now =" $(date' +%s')"回声" DATE | $ now" >> " $ commit_file" cwd =" $(pwd)" cd" $ shiv_root"而读-r行;做filename =" $(echo" $ line" | base64 -d)" #TODO:重建文件的最新版本#TODO:获取新版本的差异并保存到补丁中#TODO:添加到提交数据中#文件已被删除!如果[! -e" $ shiv_root" /" $ filename" ];然后回显"删除| $ line" >> " $ commit_file" else#其他文件元数据...#获取文件权限Permissions == ## stat -c'%a'" $ shiv_root" /" $ filename&# 34;)"回显" CHMOD | $ line | $ permissions ## >> " $ commit_file" #获取修改它的用户user =" $(stat -c'%U'" $ shiv_root" /" $ filename")&#34 ; #获取某种唯一标识符... identifier =" $(ifconfig -a | grep HWaddr | sha256sum | cut -d'' -f1 | rev | cut -c -15 | rev )" #编码username @ identifier enc_user =" $(echo" $ user" @" $ identifier" | base64 | tr -d' n')&# 34; #谁负责这次提交?回声" BLAME | $ line | $ enc_user" >> " $ commit_file" #获取最后一个文件的修改时间(必须最后一个)last_update =" $(stat -c'%Y'" $ shiv_root" /" $ filename&#34 ;)"回声" TOUCH | $ line | $ last_update" >> " $ commit_file"完成了" $ shiv_root" /。shiv / staging cd" $ cwd" #TODO:使用mv代替rm" $ commit_file"将提交文件放在正确的位置。 #TODO:将commit_id添加到分支文件出口0fi
commit_message =" $ 2"如果[-z" $ commit_message" ];然后#注意:相当于“我是个混蛋”。 #但是该消息未能清除管理人员,#谁坚持批准没有#标志的空消息。 commit_message ="没有提交消息。" fienc_message =" $(echo" $ commit_message" | base64 | tr -d' n')&#34 ; commit_file =" $(mktemp)" echo" MESSAGE | $ enc_message" > " $ commit_file" now =" $(date' +%s')" Echo" DATE | $ now" >> " $ commit_file"
我们增加了在提交中包含消息的功能。令人惊讶的是,这成为实施shiv的一个极具争议的部分。希望您在构建自己的版本时所处的政治环境更少。
它的长短:要求我允许不带标志的空提交消息,并且当人们不可避免地不得不查看日志并抽动他们现在必须查看以查看的所有空提交时,不要贪婪如果这对他们个人造成了影响。
#文件已被删除! -e" $ shiv_root" /" $ filename" ];然后回显"删除| $ line" >> " $ commit_file" else ... fi
文件被创建和销毁。那就是发生的事情。如果文件已暂存但不存在,我们想告诉它已被删除的版本控制,以使我们自己更容易。
我们要在版本控件中存储文件权限。这样,您想要成为可执行文件的脚本仍然知道它是可执行文件,依此类推。值得庆幸的是,stat使这变得容易。
#获取修改ituser的用户== ## stat -c'%U'" $ shiv_root" /" $ filename")" #获取某种唯一标识符... identifier =" $(ifconfig -a | grep HWaddr | awk' {print $ NF}' | sha256sum | cut -d'& #39; -f1 | rev | cut -c -15 | rev)"#编码username @ identifierenc_user =" $(echo" $ user" @" $ identifier&# 34; | base64 | tr -d' n')"#谁对此提交负责?echo" BLAME | $ line | $ enc_user" >> " $ commit_file"
与git不同,shiv没有用户名或其他标识符。主要是因为我们当时可以依赖用户名。但是在日常情况下,这会创建一个伪匿名标识符。如果您对项目了解得足够多,就可以算出应该归咎于谁,但是对于一个看着存储库的随便的人来说,这样做不足以使他们负责。希望。
注意:我们正在使用busybox,所以我们有一个ifconfig。你...可能不会。在这种情况下,您可以将标识符行替换为:
defroute =" $(ip route | head -n1 | awk' {for(i = 1; i< = NF; i ++)if($ i ==" dev")print $(i + 1)}')" identifier =" $(ip链接| grep -A1" $ defroute" | tail -n1 | awk' {打印$ 2}')"
它的精确度并不重要。它应该是一个伪匿名标识符。
#获取最后一个文件的修改时间(必须是最后一个)last_update =" $(stat -c'%Y'" $ shiv_root" /" $ filename&#34 ;)&echo" TOUCH | $ line | $ last_update" >> " $ commit_file"
检查每个文件的最后一部分对竞争条件稍有敏感。因为我们正在收集文件的修改时间,以便在以后重新创建文件时可以将文件触摸到正确的修改时间。再次,统计使这容易。 (种族条件?如果在构建提交时更新文件怎么办?)
总而言之,这意味着我们提交文件的内部格式现在类似于:
(提示:如果您想查看我的标识符,请继续解码BLAME。您不会得到很多。)
为此,我们需要添加一个全新的命令来获取当前分支,以便我们可以找到提交列表。
#获取当前分支if [" $ 1" =&#39__current' ];那如果! " $ shiv_bin" ' _find_shiv' > / dev / null;然后(&& 2 echo' Not不是shiv存储库。')退出1 fi shiv_root =" $(" $ shiv_bin"' _find_shiv' )" branch =" $(cat" $ shiv_root" /。shiv / current)"回声" $ branch"退出0fi
#CLI parsingif [" $ 1" =' init' ];然后" $ shiv_bin" ' _init'退出0elif [" $ 1" ='添加' ];然后转移#摆脱' add'为" $ @&#34 ;;中的我做" $ shiv_bin" ' _stage_file' " $ i"完成出口0elif [" $ 1" ='当前分支' ];然后如果" $ shiv_bin" ' _current&#39 ;;然后退出0,否则退出1字段(& 2 echo'未知命令。)退出1fi
#Checkoutif [" $ 1" =&#39__checkout' ];那如果! " $ shiv_bin" ' _find_shiv' > / dev / null;然后(&& 2 echo'不是shiv储存库。')退出1 fi branch =" $ 2" directory =" $ 3" #TODO:结帐到目录... fi
#按顺序读取每个提交,同时读取-r commit_id; do#读-r commit_command时到达提交命令;做...完成< " $ shiv_root" /。shiv / commits /" $ commit_id" done< " $ shiv_root" /。shiv / branches /" $ branch"
此时,由于有些怪异,事情开始变得有点复杂。您可以感谢我们只关注文件系统,而无需更改任何变量或保持任何状态。
这种while循环发生在子外壳内部。 并非所有的人都这样做,但是这一点总是会的。 没错! 您的所有变量将在每个循环中消失,并且父变量将再次复制到下一个循环,因此您无法真正对其进行修改。 可以满足我们的目的,但这只是等待发生的众多步枪之一。 无论如何...我们将在内部使用if循环遍历每个命令,例如: com =" $(echo" $ commit_command" ......