1 (изменено: Rumata, 2020-12-16 12:53:16)

Тема: CMD/BAT: Улучшение cmd.exe

Предисловие

Разрабатывая этот скрипт setcmd.bat,  я получил удовольствие. Хотя сам его не использую в повседневной жизни, отдавая предпочтение сторонним, более продвинутым средствам, таким как Clink, ConEmu, Far. Тем не менее считаю, что этот велосипед можно использовать.


Описание

Этот скрипт позиционируется как расширение возможностей стандартного cmd.exe, попытка сделать его чуть ближе к родственным оболочкам, например, таким как bash.


Алиасы, макрокоманды

Аналог команды doskey, но синтаксисом попроще и чуть гибче.

alias
показать все алиасы

alias name=text
определить новый алиас

alias -r [ FILENAME ]
загрузить новый список алиасов из указанного файлы FILENAME. Если файл не задан, то пытается загрузить из файла, заданного переменной %CMD_ALIASFILE%.

unalias
удалить алиас


История команд

История команд в cmd.exe не ведется совсем. Это попытка хоть как-то улучшить положение. Команды предыдущей сессии загрузить нельзя, но можно их хотя бы сохранить.

history
показать историю команд текущей сессии

history -c
очистить историю команд в текущей сессии

history -w
сохранить (дописать) текущую сессию в заданный файл. Имя файла задается переменной %CMD_HISTFILE%. Размер истории можно задать с помощью следующих переменных %CMD_HISTSIZE% - максимальное количество команд хранимых в буфере текущей сессии (по умолчанию 50). %CMD_HISTFILESIZE% - количество строк, которые можно хранить в файле истории (более старые удаляются при превышении этого значения, Если %CMD_HISTFILE% или %CMD_HISTFILESIZE% не заданы, то история не сохраняется.


Навигация

cd
показать текущий каталог

В bash эта команда вернется в домашний каталог пользователя, но я решщил оставить стандартное поведение встроенной команды cmd.exe.

cd -
перейти в предыдущий каталог.

cd ~
перейти в домашний каталог пользователя %USERPROFILE%

cd path
перейти в заданный каталог

Завершение сессии

По завершении сессии история команд сбрасывается (дописывается) в файл истории. Для этого определены следующие алиасы

exit
CTRL-D

Последний алиас это символ CTRL-D, ASCII 04, обычно отображаемый как ^D. В отличие от unix-овых оболочек, которые автоматически завершают сессию и закрывают тенрминал здесь требуется подтвердить нажатием клавиши enter.


Конфигурирование

Изменить поведение скрипта можно с помощью пользовательского скрипта setcmd.rc.bat, который может быть загружен из

а) каталога %CMD_SETCMDDIR%, где установлен скрипт setcmd.bat,
б) каталога %HOME% (если задан),
в) каталога пользователя %USERPROFILE%.

CMD_ALIASFILE
имя файла алиасов

CMD_ALIAS_NOBUILTINS
непустое значение запрещает установку встроенных алиасов (смотри список выше)

CMD_HISTFILE
имя файла истории для сохранения текущей сессии

CMD_HISTFILESIZE
предельное количество строк в файле истории

CMD_HISTSIZE
предельное количество строк в истории текущей сессии


Автозапуск

Можно настроить так, что этот скрипт будет автоматически выполнятся при каждом запуске cmd.exe

setcmd autorun -i
инсталлировать автозапуск скрипта

setcmd autorun -u
деинсталлировать завтозапуск скрипта


Исходный код

+ исходный код setcmd.bat

Оригинал скрипта и самая его свежая версия находится по адресу:
https://github.com/ildar-shaimordanov/c … setcmd.bat


::
:: The tool was developed to enhance the functionality of `cmd.exe` 
:: similar to Unix-like shells. It is completely written as batch script 
:: and does npt add any external binaries. Nevertheless, it gives more 
:: functions and flexibility to `cmd.exe` and do maintainance little bit 
:: easier. 
::
:: In fact, this script is weak attempt to be closer to other shells - 
:: powerful, flexible and full-functional ones. Nevertheless, it works! 
:: This script can be found useful for those folks, who are not permitted 
:: to setup any other binaries excepting those applications permitted for 
:: installation. The better way is to use the other solutions like 
:: `Clink`, `ConEmu` or something else. 
::
@echo off

for %%1 in (

	"help"
	"alias.readfile"
	"history"
	"cd"
	"autorun"

) do if /i "%~1" == "%%~1" (
	call :cmd.%*
	goto :EOF
)

if not "%~1" == "" (
	>&2 echo:Unknown command "%~1".
	goto :EOF
)


::
:: # USER DEFINED FILE
::
::
:: The user defined file `setcmd.rc.bat` allows to customize the user's 
:: environment. Firstly, it is checked for existance in the same directory 
:: where `setcmd.bat` is located, further it is checked under the `HOME` 
:: directory and finally under the user's profile. If these files exist 
:: they are called and affects on the further execution. Each of them can 
:: override previous settings. 
::
:: This behavior is very close to the existing in unix world when settings 
:: of `~/.bashrc` override settings of `/etc/bashrc`. 
::
:: The `CMD_SETCMDDIR` environment variable points to the directory where 
:: the main script `setcmd.bat` is located. It can be used, if some 
:: settings are placed under this directory. 
::
for /f "tokens=*" %%f in ( "%~dp0." ) do set "CMD_SETCMDDIR=%%~ff"

if exist "%~dpn0.rc.bat" call "%~dpn0.rc.bat"
if exist "%HOME%\%~n0.rc.bat" call "%HOME%\%~n0.rc.bat"
if exist "%USERPROFILE%\%~n0.rc.bat" call "%USERPROFILE%\%~n0.rc.bat"


::
:: # ENVIRONMENT VARIABLES
::
::
:: Behaviour of the script depends on some environment variables described 
:: below. Most of them have synonyms in unix and the same meaning. 
::
:: Uncomment a line if you want to turn on a feature supported by a 
:: variable. The better place for tuning all these variables is auxiliary 
:: `setcmd.rc.bat` script (see the description above). 
::
::
:: `CMD_ALIASFILE`
::
:: Define the name of the file of aliases or `DOSKEY` macros. 
::
if not defined CMD_ALIASFILE set "CMD_ALIASFILE=%~dpn0.aliases"
::
:: `CMD_ALIAS_NOBUILTINS`
::
:: Any non-empty value disables setting of builtin aliases at startup. 
::
rem if not defined CMD_ALIAS_NOBUILTINS set "CMD_ALIAS_NOBUILTINS=1"
::
:: `CMD_HISTFILE`
::
:: Define the name of the file in which command history is saved. 
::
rem if not defined CMD_HISTFILE set "CMD_HISTFILE=%~dpn0.history"
::
:: `CMD_HISTFILESIZE`
::
:: Define the maximum number of lines in the history file. 
::
rem if not defined CMD_HISTFILESIZE set /a "CMD_HISTFILESIZE=500"
::
:: `CMD_HISTSIZE`
::
:: Define the maximum number of commands remembered by the buffer. 
:: By default `DOSKEY` stores `50` latest commands in its buffer. 
::
rem if not defined CMD_HISTSIZE set /a "CMD_HISTSIZE=50"
::
:: `CMD_HISTCONTROL`
::
:: A semicolon-separated list of values controlling how commands are saved 
:: in the history file. 
::
:: **Not implemented**
::
rem if not defined CMD_HISTCONTROL set "CMD_HISTCONTROL="
::
:: `CMD_HISTIGNORE`
::
:: A semicolon-separated list of ignore patterns used to decide which 
:: command lines should be saved in the history file. 
::
:: **Not implemented**
::
rem if not defined CMD_HISTIGNORE set "CMD_HISTIGNORE="

if not defined CMD_ALIAS_NOBUILTINS call :cmd.alias.builtins
if exist "%CMD_ALIASFILE%" call :cmd.alias.readfile "%CMD_ALIASFILE%"

goto :EOF


:cmd.help
for /f "tokens=* delims=: " %%s in ( ' 
	findstr /b "::" "%~f0" 
' ) do echo:%%~s
goto :EOF


:cmd.alias.builtins
::
:: # ALIASES
::
::
:: `alias`
::
:: Display all aliases.
::
::
:: `alias name=text`
::
:: Define an alias with the name for one or more commands.
::
::
:: `alias -r [FILENAME]`
::
:: Read aliases from the specified file or `CMD_ALIASFILE`.
::
doskey alias=^
if "$1" == "" ( doskey /macros ) else ^
if "$1" == "-r" ( "%~f0" alias.readfile "$2" ) else ( doskey $* )
::
:: `unalias name`
::
:: Remove the alias specified by name from the list of defined aliases.
:: Run `DOSKEY /?` for more details.
::
doskey unalias=doskey $1=
::
:: `history`
::
:: Display or manipulate the history list for the actual session. 
:: Run `DOSKEY /?` for more details.
::
doskey history="%~f0" history $1
::
:: `cd`
::
:: Display or change working directory. 
::
doskey cd="%~f0" cd $1
::
:: `exit`
::
:: Exit the current command prompt; before exiting store the actual 
:: history list to the history file `CMD_HISTFILE` when it is configured. 
::
doskey exit=if not "$1" == "/?" ( "%~f0" history -w ) $T exit $*
::
:: `CTRL-D`
::
:: `CTRL-D` (`ASCII 04`, `EOT` or the *diamond* symbol) is useful shortcut 
:: for the `exit` command. Unlike Unix shells the `CTRL-D` keystroke 
:: doesn't close window immediately. In Windows command prompt you need to 
:: press the `ENTER` keystroke. 
::
doskey ="%~f0" history -w $T exit
goto :EOF


::
:: # ALIAS FILE
::
::
:: Alias file is the simple text file defining aliases or macros in the 
:: form `name=command` and can be loaded to the session by the prefedined 
:: alias `alias -r`.
::
:cmd.alias.readfile
setlocal

if not "%~1" == "" set "CMD_ALIASFILE=%~1"

if not defined CMD_ALIASFILE (
	endlocal
	goto :EOF
)

if not exist "%CMD_ALIASFILE%" (
	>&2 echo:"%CMD_ALIASFILE%" not found.
	endlocal
	goto :EOF
)

set /a "CMD_HISTSIZE=CMD_HISTSIZE"
if %CMD_HISTSIZE% gtr 0 doskey /LISTSIZE="%CMD_HISTSIZE%"

doskey /MACROFILE="%CMD_ALIASFILE%"

endlocal
goto :EOF


::
:: # HISTORY
::
::
:: `history [options]`
::
::
:: ## Options
::
:cmd.history
if "%~1" == ""   goto :cmd.history.print
if "%~1" == "-c" goto :cmd.history.clear
if "%~1" == "-C" goto :cmd.history.uninstall
if "%~1" == "-w" goto :cmd.history.write

>&2 echo:Unsupported history option "%~1".
goto :EOF


::
:: `history`
::
:: Displays the history of the current session.
::
:cmd.history.print
doskey /HISTORY
goto :EOF


::
:: `history -c`
::
:: Clear the history list by setting the history size to 0 and reverting 
:: to the value defined in `CMD_HISTSIZE` or `50`, the default value. 
::
:cmd.history.clear
setlocal

set /a "CMD_HISTSIZE=CMD_HISTSIZE"
if %CMD_HISTSIZE% leq 0 set CMD_HISTSIZE=50
doskey /LISTSIZE=0
doskey /LISTSIZE=%CMD_HISTSIZE%

endlocal
goto :EOF


::
:: `history -C`
::
:: Install a new copy of `DOSKEY` and clear the history buffer. This way 
:: is less reliable and deprecated in usage because of possible loss of 
:: control over the command history. 
::
:cmd.history.uninstall
doskey /REINSTALL
goto :EOF


::
:: `history -w`
::
:: Write the current history to the file `CMD_HISTFILE` if it is defined. 
::
:cmd.history.write
if not defined CMD_HISTFILE goto :EOF

doskey /HISTORY >>"%CMD_HISTFILE%" || goto :EOF

if not defined CMD_HISTFILESIZE goto :EOF

setlocal 

set /a "CMD_HISTFILESIZE=CMD_HISTFILESIZE"
set /a "CMD_FILEPOS=0"

for /f %%f in ( ' 
	"%windir%\System32\more.exe" ^< "%CMD_HISTFILE%" ^| ^
	"%windir%\System32\find.exe" /v /c "" 
' ) do (
	set /a "CMD_FILEPOS=%%f-CMD_HISTFILESIZE"
)

if %CMD_FILEPOS% leq 0 (
	endlocal
	goto :EOF
)

more +%CMD_FILEPOS% "%CMD_HISTFILE%" >"%CMD_HISTFILE%~"
move /y "%CMD_HISTFILE%~" "%CMD_HISTFILE%"

endlocal
goto :EOF


::
:: # CHANGE DIRECTORY
::
::
:: `cd [options]`
::
::
:: Change the current directory can be performed by the following commands 
:: `CD` or `CHDIR`. To change both current directory and drive the option 
:: `/D` is required. To avoid certain typing of the option and simplify 
:: navigation between the current directory, previous one and user's home 
:: directory, the command is extended as follows.
::
:: See the following links for details
::
:: * http://ss64.com/nt/pushd.html
:: * http://ss64.com/nt/popd.html
:: * http://ss64.com/nt/cd.html
::
:: There is another way how to combine `cd`, `pushd` and `popd`. You can 
:: find it following by the link:
::
:: * https://www.safaribooksonline.com/library/view/learning-the-bash/1565923472/ch04s05.html
::
::
:: `cd`
::
:: Display the current drive and directory.
::
::
:: `cd ~`
::
:: Change to the user's home directory.
::
::
:: `cd -`
::
:: Change to the previous directory. The previously visited directory is 
:: stored in the OLDCD variable. If the variable is not defined, no action 
:: happens. 
::
::
:: `cd path`
::
:: Change to the directory cpecified by the parameter.
::
:cmd.cd
if "%~1" == "-" if not defined OLDCD (
	>&2 echo:OLDCD not set
	goto :EOF
)

if "%~1" == "" (
	cd
	goto :EOF
)

setlocal

if "%~1" == "-" (
	set "NEWCD=%OLDCD%"
) else if "%~1" == "~" (
	set "NEWCD=%USERPROFILE%"
) else (
	set "NEWCD=%~1"
)

endlocal & set "OLDCD=%CD%" & cd /d "%NEWCD%"
goto :EOF


:cmd.autorun
setlocal

set "CMD_AUTORUN=HKCU\Software\Microsoft\Command Processor"

if "%~1" == "-s" (
	reg query  "%CMD_AUTORUN%" /v "AutoRun"
) else if "%~1" == "-i" (
	reg add    "%CMD_AUTORUN%" /v "AutoRun" /t REG_EXPAND_SZ /d "\"%~f0\"" /f
) else if "%~1" == "-u" (
	reg delete "%CMD_AUTORUN%" /v "AutoRun" /f
) else (
	>&2 echo:Unsupported autorun option "%~1".
)

endlocal
goto :EOF


::
:: # ADDITIONAL REFERENCES
::
:: * https://msdn.microsoft.com/ru-ru/library/windows/desktop/ee872121%28v=vs.85%29.aspx
:: * http://www.outsidethebox.ms/12669/
:: * http://www.transl-gunsmoker.ru/2010/09/11.html
:: * http://habrahabr.ru/post/263105/
::
( 2 * b ) || ! ( 2 * b )

2

Re: CMD/BAT: Улучшение cmd.exe

Выполнение команд в одной строке

@echo off
setlocal enabledelayedexpansion
Call :parse "1 #add; factorial(6) #sub; 1"
endlocal
exit /b

:parse
setlocal
set erft[1]=Syntax error, check for parenthesys allocation
set erfc[1]=Error #1 : Bracket friend has not been detected "("

set erft[2]=Syntax error, check for parenthesys allocation
set erfc[2]=Error #2 : Undeclared end ")"

set erft[3]=Syntax error, check for functions
set erfc[3]=Error #3 : Undefined ";"

set erft[4]=Syntax error, check for functions
set erfc[4]=Error #4 : Undefined "#"

set erft[5]=Syntax error, check for functions
set erfc[5]=Error #5 : In "#" and ";" is not allowed to place spaces and brackets

set erft[6]=Syntax error, check for functions
set erfc[6]=Error #6 : Missing last argument

set output=%2
set t=%~1
Call :getsize "!t!" a
set /a b=a - 1
rem Check for syntax correct
set par=0
set err=0
set errpos=
set stat=0
for /l %%i in (0,1,!b!) do (
	if not "!t:~%%i,1!"==" " if !stat!==2 set stat=0
	if "!t:~%%i,1!"=="(" set /a par+=1
	if "!t:~%%i,1!"==")" set /a par-=1
	if "!t:~%%i,1!"=="#" set stat=1
	if "!t:~%%i,1!"==" " if !stat!==1 set err=5&set errpos=%%i
	if "!t:~%%i,1!"=="(" if !stat!==1 set err=5&set errpos=%%i
	if "!t:~%%i,1!"==")" if !stat!==1 set err=5&set errpos=%%i
	if "!t:~%%i,1!"==";" (
		if !stat!==1 (
			set stat=2
		) else (
			set err=4
			set errpos=%%i
		)
	)
	if !par! lss 0 set err=1&set errpos=%%i
)
if !par! gtr 0 set err=2
if !stat!==1 set err=3
if !stat!==2 set err=6
rem Fall with error
if !err! neq 0 (
	if defined errpos Echo Syntax error at : !errpos!
	Echo !erft[%err%]!
	Echo !erfc[%err%]!
	goto :eof
)
rem Parse First Brackets
if not "!t!"=="!t:(=!" (
	rem Get last and first brackets
	rem Echo DEBUG: "!t!"
	rem Find first closing symbol
	set opengate=0
	set opengatepos=
	for /l %%i in (0,1,!b!) do (
		if !opengate!==0 (
			if "!t:~%%i,1!"==")" set opengate=1&set opengatepos=%%i
		)
	)
	rem Echo !opengatepos!
	rem Find first opening symbol
	set closegate=0
	set closegatepos=
	for /l %%i in (!opengatepos!,-1,0) do (
		if !closegate!==0 (
			if "!t:~%%i,1!"=="(" set closegate=1&set closegatepos=%%i
		)
	)
	rem Echo !closegatepos!
	rem Distance beetwen close and open brackets
	set /a "abs=opengatepos-closegatepos-1"
	rem Us 007
	set /a "tmp001=closegatepos + 1"
	set /a "tmp003=opengatepos+1"
	set /a "tmp006=closegatepos-1"
	rem Echo !abs!
	rem wordname detector
	if !closegatepos!==0 (
		rem [SOF] "(" exp ")"
		Call :substr t !tmp001! !abs! tmp002
		rem Echo DEBUG:  !tmp002!
		Call :substr t !tmp003! -1 tmp004
		Call :parse "!tmp002!" result
		rem Echo !tmp004!
	) else (
		rem ??? but not [SOF] "(" exp ")"
		rem [function name] without '#' and ';' "(" exp ")"
		rem [brackets] with '#' and ';' "(" exp ")"
		set nameoffunc=
		set parsefuncname=0
		set w=0
		for /l %%i in (!tmp006!,-1,0) do (
			if !parsefuncname!==0 (
				if "!t:~%%i,1!"=="(" set parsefuncname=1
				if "!t:~%%i,1!"==")" set parsefuncname=1
				if "!t:~%%i,1!"==" " set parsefuncname=1
				if "!t:~%%i,1!"==";" set parsefuncname=1
				if !parsefuncname!==0 (
					set nameoffunc=!t:~%%i,1!!nameoffunc!
					set w=%%i
				)
			)
		)
		if defined nameoffunc (
			if !abs!==0 (
				rem ???
			) else (
				Call :substr t !tmp001! !abs! tmp007
				Call :parser-!nameoffunc! result !tmp007!
				rem Echo DEBUG:  '!tmp007!'
				Call :substr t 0 !w! tmp008
				Call :substr t !tmp003! 9999 tmp009
				Call :parse "!tmp008!!result!!tmp009!"
				rem Echo DEBUG:  '!tmp008!'!result!'!tmp009!'
			)
		) else (
			rem ???
		)
	)
	
) else if not "!t!"=="!t:#=!" (
	set state=0
	set arga=
	set argb=
	set nameoffunc=
	for /l %%i in (0,1,!b!) do (
		if !state!==0 if "!t:~%%i,1!"=="#" set state=1&set topos1=%%i
		if !state!==1 if "!t:~%%i,1!"==";" set state=2&set topos2=%%i
		if !state!==2 if "!t:~%%i,1!"=="#" set state=3&set topos3=%%i
		if !state! leq 2 (
			if not "!t:~%%i,1!"==" " (
			if !state!==0 set arga=!arga!!t:~%%i,1!
			if not "!t:~%%i,1!"==";" if !state!==2 set argb=!argb!!t:~%%i,1!
			if not "!t:~%%i,1!"=="#" if !state!==1 set nameoffunc=!nameoffunc!!t:~%%i,1!
			)
		)
	)
	Call :substr t !topos3! 9999 tast
	Call :parser-!nameoffunc! return !arga! !argb!
	Call :parse "!return!!tast!"
) else (
	Echo Result : !t!
)
endlocal
goto :eof

:substr
set %4=!%1:~%2,%3!
goto :eof

:getsize
setlocal enabledelayedexpansion
set min=0&set max=8192
set text=%~1
if "!text!"=="" set size=0&goto getsize_end
	:getsize_loop
	set /a am=(min+max) / 2
	set /a amp=am + 1
	if "!text:~%am%,1!"=="" set max=!am!&goto getsize_loop
	if not "!text:~%amp%,1!"=="" set min=!am!&goto getsize_loop
	set size=!am!
set /a size+=1
:getsize_end
endlocal&set %2=%size%
goto :eof

:parser-factorial
set %1=1
for /l %%i in (1,1,%2) do (
	set /a %1*=%%i
)
goto :eof

:parser-add
set /a %1=%2+%3
goto :eof

:parser-sub
set /a %1=%2-%3
goto :eof

:parser-mul
set /a %1=%2*%3
goto :eof

:parser-div
set /a %1=%2/%3
goto :eof